@nifrajs/cli 4.0.2 → 5.0.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 (323) hide show
  1. package/README.md +3 -0
  2. package/dist/agent-files.d.ts +107 -0
  3. package/dist/agent-files.d.ts.map +1 -0
  4. package/dist/agent-files.js +162 -0
  5. package/dist/agent-files.js.map +1 -0
  6. package/dist/app-files.d.ts +6 -0
  7. package/dist/app-files.d.ts.map +1 -1
  8. package/dist/app-files.js +27 -2
  9. package/dist/app-files.js.map +1 -1
  10. package/dist/assure-hydration.d.ts.map +1 -1
  11. package/dist/assure-hydration.js +7 -2
  12. package/dist/assure-hydration.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts.map +1 -1
  14. package/dist/capabilities-tool.js +13 -3
  15. package/dist/capabilities-tool.js.map +1 -1
  16. package/dist/check-diagnostics.d.ts +20 -2
  17. package/dist/check-diagnostics.d.ts.map +1 -1
  18. package/dist/check-diagnostics.js +54 -7
  19. package/dist/check-diagnostics.js.map +1 -1
  20. package/dist/check-scan.d.ts +75 -22
  21. package/dist/check-scan.d.ts.map +1 -1
  22. package/dist/check-scan.js +1475 -310
  23. package/dist/check-scan.js.map +1 -1
  24. package/dist/check.d.ts +2 -2
  25. package/dist/check.d.ts.map +1 -1
  26. package/dist/check.js +192 -65
  27. package/dist/check.js.map +1 -1
  28. package/dist/cli.d.ts +7 -0
  29. package/dist/cli.d.ts.map +1 -1
  30. package/dist/cli.js +112 -5
  31. package/dist/cli.js.map +1 -1
  32. package/dist/client-module.d.ts +18 -0
  33. package/dist/client-module.d.ts.map +1 -0
  34. package/dist/client-module.js +49 -0
  35. package/dist/client-module.js.map +1 -0
  36. package/dist/command-catalog.d.ts +16 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +133 -29
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/component-check.d.ts +13 -0
  41. package/dist/component-check.d.ts.map +1 -0
  42. package/dist/component-check.js +59 -0
  43. package/dist/component-check.js.map +1 -0
  44. package/dist/contract-proof.d.ts +1 -1
  45. package/dist/contract-proof.d.ts.map +1 -1
  46. package/dist/contract-proof.js +2 -24
  47. package/dist/contract-proof.js.map +1 -1
  48. package/dist/contracts.d.ts +19 -0
  49. package/dist/contracts.d.ts.map +1 -1
  50. package/dist/contracts.js +50 -3
  51. package/dist/contracts.js.map +1 -1
  52. package/dist/db-tool.d.ts.map +1 -1
  53. package/dist/db-tool.js +4 -3
  54. package/dist/db-tool.js.map +1 -1
  55. package/dist/dev-bun-config.d.ts +5 -1
  56. package/dist/dev-bun-config.d.ts.map +1 -1
  57. package/dist/dev-bun-config.js +44 -11
  58. package/dist/dev-bun-config.js.map +1 -1
  59. package/dist/diagnostics.d.ts +3 -0
  60. package/dist/diagnostics.d.ts.map +1 -1
  61. package/dist/diagnostics.js.map +1 -1
  62. package/dist/diff-tool.d.ts +9 -8
  63. package/dist/diff-tool.d.ts.map +1 -1
  64. package/dist/diff-tool.js +28 -24
  65. package/dist/diff-tool.js.map +1 -1
  66. package/dist/doctor.d.ts +13 -0
  67. package/dist/doctor.d.ts.map +1 -1
  68. package/dist/doctor.js +23 -15
  69. package/dist/doctor.js.map +1 -1
  70. package/dist/env-file.d.ts +6 -0
  71. package/dist/env-file.d.ts.map +1 -1
  72. package/dist/env-file.js +6 -0
  73. package/dist/env-file.js.map +1 -1
  74. package/dist/fix-recipes.d.ts.map +1 -1
  75. package/dist/fix-recipes.js +21 -2
  76. package/dist/fix-recipes.js.map +1 -1
  77. package/dist/hand-built-manifest.d.ts +27 -0
  78. package/dist/hand-built-manifest.d.ts.map +1 -0
  79. package/dist/hand-built-manifest.js +124 -0
  80. package/dist/hand-built-manifest.js.map +1 -0
  81. package/dist/hooks-report.d.ts +23 -0
  82. package/dist/hooks-report.d.ts.map +1 -0
  83. package/dist/hooks-report.js +104 -0
  84. package/dist/hooks-report.js.map +1 -0
  85. package/dist/init-agents.d.ts +2 -2
  86. package/dist/init-agents.d.ts.map +1 -1
  87. package/dist/init-agents.js +17 -11
  88. package/dist/init-agents.js.map +1 -1
  89. package/dist/internal/nifra-project.d.ts +41 -0
  90. package/dist/internal/nifra-project.d.ts.map +1 -0
  91. package/dist/internal/nifra-project.js +183 -0
  92. package/dist/internal/nifra-project.js.map +1 -0
  93. package/dist/internal/project-specifiers.d.ts +16 -0
  94. package/dist/internal/project-specifiers.d.ts.map +1 -0
  95. package/dist/internal/project-specifiers.js +396 -0
  96. package/dist/internal/project-specifiers.js.map +1 -0
  97. package/dist/internal/script-kind.d.ts.map +1 -1
  98. package/dist/internal/script-kind.js +2 -1
  99. package/dist/internal/script-kind.js.map +1 -1
  100. package/dist/internal/source-facts.d.ts +2 -0
  101. package/dist/internal/source-facts.d.ts.map +1 -1
  102. package/dist/internal/source-facts.js +11 -0
  103. package/dist/internal/source-facts.js.map +1 -1
  104. package/dist/internal/typescript-import.d.ts +1 -1
  105. package/dist/internal/typescript-import.d.ts.map +1 -1
  106. package/dist/internal/typescript-import.js +5 -3
  107. package/dist/internal/typescript-import.js.map +1 -1
  108. package/dist/introspect.d.ts +6 -6
  109. package/dist/introspect.d.ts.map +1 -1
  110. package/dist/introspect.js +55 -24
  111. package/dist/introspect.js.map +1 -1
  112. package/dist/levels-tool.d.ts.map +1 -1
  113. package/dist/levels-tool.js +93 -15
  114. package/dist/levels-tool.js.map +1 -1
  115. package/dist/load.d.ts +31 -8
  116. package/dist/load.d.ts.map +1 -1
  117. package/dist/load.js +37 -7
  118. package/dist/load.js.map +1 -1
  119. package/dist/mcp-context.d.ts +5 -5
  120. package/dist/mcp-context.d.ts.map +1 -1
  121. package/dist/mcp-context.js +2 -2
  122. package/dist/mcp-context.js.map +1 -1
  123. package/dist/mcp-exec.d.ts +12 -4
  124. package/dist/mcp-exec.d.ts.map +1 -1
  125. package/dist/mcp-exec.js +50 -13
  126. package/dist/mcp-exec.js.map +1 -1
  127. package/dist/mcp-http.js +1 -1
  128. package/dist/mcp-io.d.ts +17 -0
  129. package/dist/mcp-io.d.ts.map +1 -1
  130. package/dist/mcp-io.js +40 -0
  131. package/dist/mcp-io.js.map +1 -1
  132. package/dist/mcp-isolate.d.ts.map +1 -1
  133. package/dist/mcp-isolate.js +3 -2
  134. package/dist/mcp-isolate.js.map +1 -1
  135. package/dist/mcp-project-child.js +1 -1
  136. package/dist/mcp-project-child.js.map +1 -1
  137. package/dist/mcp-root.d.ts.map +1 -1
  138. package/dist/mcp-root.js +50 -10
  139. package/dist/mcp-root.js.map +1 -1
  140. package/dist/mcp-ws.d.ts.map +1 -1
  141. package/dist/mcp-ws.js +3 -1
  142. package/dist/mcp-ws.js.map +1 -1
  143. package/dist/mcp.d.ts.map +1 -1
  144. package/dist/mcp.js +8 -2
  145. package/dist/mcp.js.map +1 -1
  146. package/dist/migrate-layout.d.ts +7 -0
  147. package/dist/migrate-layout.d.ts.map +1 -1
  148. package/dist/migrate-layout.js +778 -64
  149. package/dist/migrate-layout.js.map +1 -1
  150. package/dist/output-schemas.d.ts +94 -0
  151. package/dist/output-schemas.d.ts.map +1 -0
  152. package/dist/output-schemas.js +864 -0
  153. package/dist/output-schemas.js.map +1 -0
  154. package/dist/port.d.ts.map +1 -1
  155. package/dist/port.js +3 -2
  156. package/dist/port.js.map +1 -1
  157. package/dist/project-facts.d.ts +15 -4
  158. package/dist/project-facts.d.ts.map +1 -1
  159. package/dist/project-facts.js +1 -0
  160. package/dist/project-facts.js.map +1 -1
  161. package/dist/recipes/5.0.0.d.ts +11 -0
  162. package/dist/recipes/5.0.0.d.ts.map +1 -0
  163. package/dist/recipes/5.0.0.js +34 -0
  164. package/dist/recipes/5.0.0.js.map +1 -0
  165. package/dist/recipes/index.d.ts +7 -0
  166. package/dist/recipes/index.d.ts.map +1 -1
  167. package/dist/recipes/index.js +2 -0
  168. package/dist/recipes/index.js.map +1 -1
  169. package/dist/replay.d.ts.map +1 -1
  170. package/dist/replay.js +12 -3
  171. package/dist/replay.js.map +1 -1
  172. package/dist/review.d.ts +3 -1
  173. package/dist/review.d.ts.map +1 -1
  174. package/dist/review.js +38 -7
  175. package/dist/review.js.map +1 -1
  176. package/dist/rules/codes.d.ts +10 -0
  177. package/dist/rules/codes.d.ts.map +1 -1
  178. package/dist/rules/codes.js +10 -0
  179. package/dist/rules/codes.js.map +1 -1
  180. package/dist/rules/comment-markers.d.ts +16 -7
  181. package/dist/rules/comment-markers.d.ts.map +1 -1
  182. package/dist/rules/comment-markers.js +58 -19
  183. package/dist/rules/comment-markers.js.map +1 -1
  184. package/dist/rules/contract-visibility.d.ts +3 -0
  185. package/dist/rules/contract-visibility.d.ts.map +1 -0
  186. package/dist/rules/contract-visibility.js +337 -0
  187. package/dist/rules/contract-visibility.js.map +1 -0
  188. package/dist/rules/data-guard.d.ts +7 -0
  189. package/dist/rules/data-guard.d.ts.map +1 -1
  190. package/dist/rules/data-guard.js +319 -37
  191. package/dist/rules/data-guard.js.map +1 -1
  192. package/dist/rules/layout.d.ts +4 -0
  193. package/dist/rules/layout.d.ts.map +1 -0
  194. package/dist/rules/layout.js +19 -0
  195. package/dist/rules/layout.js.map +1 -0
  196. package/dist/rules/legacy.d.ts.map +1 -1
  197. package/dist/rules/legacy.js +158 -43
  198. package/dist/rules/legacy.js.map +1 -1
  199. package/dist/rules/pages.d.ts +24 -0
  200. package/dist/rules/pages.d.ts.map +1 -1
  201. package/dist/rules/pages.js +221 -3
  202. package/dist/rules/pages.js.map +1 -1
  203. package/dist/rules/routes.d.ts +6 -0
  204. package/dist/rules/routes.d.ts.map +1 -1
  205. package/dist/rules/routes.js +111 -16
  206. package/dist/rules/routes.js.map +1 -1
  207. package/dist/rules/secrets.js +2 -2
  208. package/dist/rules/secrets.js.map +1 -1
  209. package/dist/rules/security.d.ts +12 -1
  210. package/dist/rules/security.d.ts.map +1 -1
  211. package/dist/rules/security.js +281 -68
  212. package/dist/rules/security.js.map +1 -1
  213. package/dist/rules/zones.d.ts +13 -0
  214. package/dist/rules/zones.d.ts.map +1 -1
  215. package/dist/rules/zones.js +192 -40
  216. package/dist/rules/zones.js.map +1 -1
  217. package/dist/scaffold.d.ts.map +1 -1
  218. package/dist/scaffold.js +6 -4
  219. package/dist/scaffold.js.map +1 -1
  220. package/dist/sdk.d.ts.map +1 -1
  221. package/dist/sdk.js +14 -5
  222. package/dist/sdk.js.map +1 -1
  223. package/dist/stylex-migrate.d.ts.map +1 -1
  224. package/dist/stylex-migrate.js +163 -7
  225. package/dist/stylex-migrate.js.map +1 -1
  226. package/dist/test-tool.d.ts.map +1 -1
  227. package/dist/test-tool.js +12 -3
  228. package/dist/test-tool.js.map +1 -1
  229. package/dist/typecheck-coverage.d.ts +20 -0
  230. package/dist/typecheck-coverage.d.ts.map +1 -0
  231. package/dist/typecheck-coverage.js +129 -0
  232. package/dist/typecheck-coverage.js.map +1 -0
  233. package/dist/upgrade.d.ts +69 -0
  234. package/dist/upgrade.d.ts.map +1 -1
  235. package/dist/upgrade.js +420 -77
  236. package/dist/upgrade.js.map +1 -1
  237. package/dist/verification-plan.d.ts.map +1 -1
  238. package/dist/verification-plan.js +11 -0
  239. package/dist/verification-plan.js.map +1 -1
  240. package/dist/version-drift.d.ts +22 -0
  241. package/dist/version-drift.d.ts.map +1 -0
  242. package/dist/version-drift.js +42 -0
  243. package/dist/version-drift.js.map +1 -0
  244. package/dist/web-app-options.d.ts +1 -1
  245. package/dist/web-app-options.d.ts.map +1 -1
  246. package/dist/web-app-options.js +1 -1
  247. package/dist/web-app-options.js.map +1 -1
  248. package/dist/work-graph.d.ts.map +1 -1
  249. package/dist/work-graph.js +20 -8
  250. package/dist/work-graph.js.map +1 -1
  251. package/docs/examples.json +54 -6
  252. package/docs/llms-full.txt +1539 -138
  253. package/docs/types.json +1456 -206
  254. package/package.json +11 -12
  255. package/src/agent-files.ts +203 -0
  256. package/src/app-files.ts +34 -4
  257. package/src/assure-hydration.ts +15 -2
  258. package/src/capabilities-tool.ts +15 -3
  259. package/src/check-diagnostics.ts +73 -9
  260. package/src/check-scan.ts +1773 -381
  261. package/src/check.ts +237 -76
  262. package/src/cli.ts +141 -5
  263. package/src/client-module.ts +61 -0
  264. package/src/command-catalog.ts +166 -37
  265. package/src/component-check.ts +79 -0
  266. package/src/contract-proof.ts +3 -25
  267. package/src/contracts.ts +60 -3
  268. package/src/db-tool.ts +5 -2
  269. package/src/dev-bun-config.ts +51 -10
  270. package/src/diagnostics.ts +3 -0
  271. package/src/diff-tool.ts +33 -26
  272. package/src/doctor.ts +43 -21
  273. package/src/env-file.ts +7 -0
  274. package/src/fix-recipes.ts +25 -2
  275. package/src/hand-built-manifest.ts +144 -0
  276. package/src/hooks-report.ts +135 -0
  277. package/src/init-agents.ts +17 -11
  278. package/src/internal/nifra-project.ts +191 -0
  279. package/src/internal/project-specifiers.ts +424 -0
  280. package/src/internal/script-kind.ts +2 -1
  281. package/src/internal/source-facts.ts +25 -0
  282. package/src/internal/typescript-import.ts +10 -3
  283. package/src/introspect.ts +65 -27
  284. package/src/levels-tool.ts +117 -20
  285. package/src/load.ts +78 -14
  286. package/src/mcp-context.ts +7 -7
  287. package/src/mcp-exec.ts +60 -17
  288. package/src/mcp-http.ts +1 -1
  289. package/src/mcp-io.ts +41 -0
  290. package/src/mcp-isolate.ts +9 -2
  291. package/src/mcp-project-child.ts +1 -1
  292. package/src/mcp-root.ts +58 -8
  293. package/src/mcp-ws.ts +4 -2
  294. package/src/mcp.ts +8 -3
  295. package/src/migrate-layout.ts +885 -63
  296. package/src/output-schemas.ts +1084 -0
  297. package/src/port.ts +3 -2
  298. package/src/project-facts.ts +17 -3
  299. package/src/recipes/5.0.0.ts +35 -0
  300. package/src/recipes/index.ts +10 -0
  301. package/src/replay.ts +14 -3
  302. package/src/review.ts +39 -10
  303. package/src/rules/codes.ts +10 -0
  304. package/src/rules/comment-markers.ts +57 -21
  305. package/src/rules/contract-visibility.ts +383 -0
  306. package/src/rules/data-guard.ts +351 -42
  307. package/src/rules/layout.ts +22 -0
  308. package/src/rules/legacy.ts +184 -51
  309. package/src/rules/pages.ts +238 -3
  310. package/src/rules/routes.ts +124 -16
  311. package/src/rules/secrets.ts +2 -2
  312. package/src/rules/security.ts +352 -71
  313. package/src/rules/zones.ts +205 -38
  314. package/src/scaffold.ts +6 -4
  315. package/src/sdk.ts +14 -5
  316. package/src/stylex-migrate.ts +134 -7
  317. package/src/test-tool.ts +18 -3
  318. package/src/typecheck-coverage.ts +134 -0
  319. package/src/upgrade.ts +487 -71
  320. package/src/verification-plan.ts +24 -0
  321. package/src/version-drift.ts +67 -0
  322. package/src/web-app-options.ts +2 -2
  323. package/src/work-graph.ts +17 -12
package/src/port.ts CHANGED
@@ -310,9 +310,10 @@ export function scanFileForFeatures(file: string, content: string, state: ScanSt
310
310
  }
311
311
 
312
312
  // Mirror nifra check/doctor's ignore set: deps, build output, generated/per-runtime dist dirs, VCS.
313
- // PLUS *.config.* and obvious build scripts (build*.ts) - `port` scans request/app source, not tooling.
313
+ // PLUS *.config.* and build scripts (build.ts, build-<target>.ts, build.<x>.ts) - `port` scans
314
+ // request/app source, not tooling. A source file that merely starts with "build" is still source.
314
315
  const IGNORED =
315
- /(^|\/)(node_modules|dist(-[a-z0-9]+)?|build|\.nifra|\.git|\.wrangler|coverage)\/|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)[^/]*\.config\.[cm]?[jt]sx?$|(^|\/)build[^/]*\.[cm]?[jt]sx?$/
316
+ /(^|\/)(node_modules|dist(-[a-z0-9]+)?|build|\.nifra|\.git|\.wrangler|coverage)\/|\.(test|spec)\.[cm]?[jt]sx?$|(^|\/)[^/]*\.config\.[cm]?[jt]sx?$|(^|\/)build(?:[-.][^/]*)?\.[cm]?[jt]sx?$/
316
317
 
317
318
  /** Walk the project's source (same surface as `nifra check`, minus config + build scripts). */
318
319
  async function walkPortSource(
@@ -7,8 +7,11 @@ import type {
7
7
  import type {
8
8
  ManifestDriftFinding,
9
9
  SourceFinding,
10
+ SqlFinding,
10
11
  StaticRouteFinding,
11
12
  TransitiveServerImportFinding,
13
+ TypedClientFinding,
14
+ UnresolvedRoutePathFinding,
12
15
  } from "./check-scan.ts"
13
16
  import type { DoctorResult } from "./doctor.ts"
14
17
  import type { TypeScriptSession } from "./internal/typescript-import.ts"
@@ -16,13 +19,15 @@ import type { PipelineReport } from "./pipeline-report.ts"
16
19
  import type { RulePack, SourceIndex } from "./rules/index.ts"
17
20
 
18
21
  export interface ProjectSourceFindings {
19
- readonly fetches: readonly SourceFinding[]
22
+ readonly fetches: readonly TypedClientFinding[]
20
23
  /** Hand-rolled `new EventSource("/…")` / `new WebSocket("/…")` to the app's own streaming surface. */
21
- readonly streams: readonly SourceFinding[]
24
+ readonly streams: readonly TypedClientFinding[]
22
25
  readonly untypedClients: readonly SourceFinding[]
23
26
  readonly removedImports: readonly SourceFinding[]
24
27
  readonly responseRoutes: readonly SourceFinding[]
25
- readonly interpolatedSql: readonly SourceFinding[]
28
+ readonly interpolatedSql: readonly SqlFinding[]
29
+ /** Route registrations whose path is neither a literal nor a constant the scan resolves. */
30
+ readonly routePaths: readonly UnresolvedRoutePathFinding[]
26
31
  }
27
32
 
28
33
  export interface ProjectPackageFacts {
@@ -41,6 +46,10 @@ export interface ContractCheckFacts {
41
46
  readonly present: boolean
42
47
  readonly vacuous: boolean
43
48
  readonly diagnostics: readonly { readonly route?: string; readonly message: string }[]
49
+ /** `METHOD /path` of every route `backend/app.ts` serves; set only when the lock check loaded it. */
50
+ readonly served?: readonly string[]
51
+ /** `METHOD /path` of each authenticated route that validates its input before its auth hook runs. */
52
+ readonly inputBeforeAuth?: readonly string[]
44
53
  readonly error?: string
45
54
  }
46
55
 
@@ -50,6 +59,10 @@ export interface ProjectCheckFacts {
50
59
  readonly sqlCompilerAvailable: boolean
51
60
  /** Set when a project TypeScript install is present but outside the scanner adapter range. */
52
61
  readonly unsupportedTypeScriptVersion?: string
62
+ /** Why component modules could not be checked: the project's TypeScript has no compiler API. */
63
+ readonly componentCheckUnsupported?:
64
+ | { readonly version: string; readonly message: string }
65
+ | undefined
53
66
  /** Shared syntax/checker session for built-in rules; omitted from serialized check output. */
54
67
  readonly typescriptSession?: TypeScriptSession
55
68
  readonly checkConfigError?: string
@@ -87,6 +100,7 @@ export function freezeProjectFacts(seed: ProjectFactsSeed): ProjectFacts {
87
100
  removedImports: freezeFindings(seed.sourceFindings.removedImports),
88
101
  responseRoutes: freezeFindings(seed.sourceFindings.responseRoutes),
89
102
  interpolatedSql: freezeFindings(seed.sourceFindings.interpolatedSql),
103
+ routePaths: freezeFindings(seed.sourceFindings.routePaths),
90
104
  })
91
105
  const policies = Object.freeze({
92
106
  ...seed.policies,
@@ -0,0 +1,35 @@
1
+ import type { UpgradeRecipe } from "./index.ts"
2
+
3
+ /**
4
+ * Upgrade recipe for Nifra 5.0.
5
+ *
6
+ * The runner pins the fixed group and lists every retired `@nifra-gate-reviewed` comment. It never
7
+ * writes the `nifra-expect` comment that replaces one: a reason it made up would silence a finding
8
+ * nobody reviewed. Every changed default below has a `nifra check` finding or a one-line way to keep
9
+ * the 4.x behavior, so the notes name both.
10
+ */
11
+ export const recipe: UpgradeRecipe = {
12
+ version: "5.0.0",
13
+ pins: [
14
+ { match: "@nifrajs/", to: "5.0.0" },
15
+ { match: "create-nifra", to: "5.0.0" },
16
+ { match: "nifra", to: "5.0.0" },
17
+ ],
18
+ importMoves: [],
19
+ retiredMarkers: [
20
+ {
21
+ marker: "@nifra-gate-reviewed",
22
+ instead: "// nifra-expect <code>: <reason>, with the code nifra check reports there",
23
+ },
24
+ ],
25
+ notes: [
26
+ "`@nifra-gate-reviewed` silences nothing. After review, silence one finding with `// nifra-expect <code>: <reason>` on its line or anywhere in the comment block above it. The upgrade lists every old comment and the line it covered; it never writes a reason. Every security finding and every error needs the reason, so `// nifra-expect route-overlap` (NF-C024, NF-C025) without one no longer silences the overlap.",
27
+ "`nifra check` reports a secret comparison (NF-S002) as advisory only in browser code inside a nifra web app: a directory with `nifra.config.ts` or `backend/framework.ts`, or whose nearest package.json depends on `@nifrajs/web` or a `@nifrajs/web-*` adapter. The same comparison in `routes/` or `frontend/` of an API-only project, or of a package without a web dependency, fails the check.",
28
+ "A WebSocket handshake runs the route chain an HTTP `GET` registered at the same point runs: `authenticate()`, auth middleware, `derive` and `beforeHandle` added before `app.ws()` guard the socket and can refuse the handshake with the response they send an HTTP request. Register a socket before the guard to keep it public. `nifra routes --hooks` lists what each socket runs.",
29
+ '`createWebApp` serves the backend over HTTP only at an `apiPrefix` the app sets; without one it is loader-only (`ctx.api`) and a browser call to `/api` reaches page routing. `nifra check` reports a `backend/framework.ts` that sets none (NF-C035); run `nifra fix --code NF-C035` to add `export const apiPrefix = "/api"` and keep the 4.x mount, or export `""` to keep the backend loader-only. A hand-written `createWebApp` call passes `apiPrefix: "/api"` itself.',
30
+ "Root status pages (`_404.tsx`, `_<status>.tsx`) and `_error.tsx` render inside the layouts above them with their loader data, and a page rendered with layout data is `private, no-store`. A root status page that imports the root layout to wrap itself now renders it twice: `nifra check` reports it (NF-C034); remove the wrap. `export const inLayouts = false` renders a page the 4.x way and runs no layout loader for it.",
31
+ "A Response returned to more than one request is no longer shared: a body-less one is copied for each request before cookies and headers are written, and one whose body was already sent answers `500`. Build a Response per request, or return data and let the handler serialize it.",
32
+ "`invariants.executor` in `nifra.assurance.ts` is optional now that `invariants.setup` can provision the target, so code that reads the config's type handles a missing `executor`.",
33
+ "A layout's `meta` receives its own loader data as `data`, `null` when the layout has no loader; the page's data moved to `pageData`. A layout `meta` written against the page's data, such as a title template, reads `pageData` now: `({ pageData }) => ({ title: pageData.title })`. `nifra check` reports a layout with no loader whose `meta` reads `data` (NF-C040). A page's own `meta` is unchanged.",
34
+ ],
35
+ }
@@ -8,6 +8,7 @@ import { recipe as v1_8_0 } from "./1.8.0.ts"
8
8
  import { recipe as v2_0_0 } from "./2.0.0.ts"
9
9
  import { recipe as v3_0_0 } from "./3.0.0.ts"
10
10
  import { recipe as v4_0_0 } from "./4.0.0.ts"
11
+ import { recipe as v5_0_0 } from "./5.0.0.ts"
11
12
 
12
13
  /** A pin rule: every dependency whose name starts with `match` is set to version `to`. */
13
14
  export interface PinRule {
@@ -30,11 +31,19 @@ export interface ImportMove {
30
31
  readonly to: string
31
32
  }
32
33
 
34
+ /** A source comment a release stopped honoring. The runner lists each one; it never rewrites it. */
35
+ export interface RetiredMarker {
36
+ readonly marker: string
37
+ /** What to write instead, printed beside each comment. */
38
+ readonly instead: string
39
+ }
40
+
33
41
  export interface UpgradeRecipe {
34
42
  readonly version: string
35
43
  readonly pins: readonly PinRule[]
36
44
  readonly dependencyMoves?: readonly DependencyMove[]
37
45
  readonly importMoves: readonly ImportMove[]
46
+ readonly retiredMarkers?: readonly RetiredMarker[]
38
47
  /** Human notes printed after the plan - e.g. structural changes the runner deliberately can't do. */
39
48
  readonly notes?: readonly string[]
40
49
  }
@@ -44,6 +53,7 @@ const RECIPES: Record<string, UpgradeRecipe> = {
44
53
  [v2_0_0.version]: v2_0_0,
45
54
  [v3_0_0.version]: v3_0_0,
46
55
  [v4_0_0.version]: v4_0_0,
56
+ [v5_0_0.version]: v5_0_0,
47
57
  }
48
58
 
49
59
  export function getRecipe(version: string): UpgradeRecipe | undefined {
package/src/replay.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { resolve, sep } from "node:path"
1
+ import { realpath } from "node:fs/promises"
2
+ import { isAbsolute, relative, resolve } from "node:path"
2
3
  import { type CompatibleReplayFile, parseCompatibleReplayFile } from "@nifrajs/core/replay"
3
4
  import { runHydrationAssurance } from "./assure-hydration.ts"
4
5
  import { checkContractsLock } from "./contracts.ts"
@@ -12,10 +13,20 @@ export interface ReplayResult {
12
13
  readonly replays?: readonly string[]
13
14
  }
14
15
 
16
+ const within = (root: string, path: string): boolean => {
17
+ const rel = relative(root, path)
18
+ return !rel.startsWith("..") && !isAbsolute(rel)
19
+ }
20
+
21
+ const canonical = (path: string): Promise<string> => realpath(path).catch(() => path)
22
+
15
23
  /** Validate and dispatch one token-only replay while keeping the file inside the project root. */
16
24
  export async function runReplay(cwd: string, file: string): Promise<ReplayResult> {
17
- const path = resolve(cwd, file)
18
- if (path !== cwd && !path.startsWith(cwd + sep)) {
25
+ const root = resolve(cwd)
26
+ // Compared as real paths: the MCP host hands over an already-resolved file, which never starts
27
+ // with a root reached through a symlink, and a replay file that is itself a link must not lead out.
28
+ const path = await canonical(resolve(root, file))
29
+ if (!within(await canonical(root), path) && !within(root, path)) {
19
30
  throw new Error("replay file must stay inside the project root")
20
31
  }
21
32
  const replay = parseCompatibleReplayFile(JSON.parse(await Bun.file(path).text()))
package/src/review.ts CHANGED
@@ -16,7 +16,7 @@ import type {
16
16
  import type { CheckResult } from "./check.ts"
17
17
  import type { CommandCtx, CommandSpec } from "./command-catalog.ts"
18
18
  import type { Diagnostic } from "./diagnostics.ts"
19
- import { toSarifLog } from "./diagnostics.ts"
19
+ import { diagnosticCompatibilityOf, toSarifLog } from "./diagnostics.ts"
20
20
  import { RULE_CODES } from "./rules/codes.ts"
21
21
  import { LEGACY_RULE_CODES } from "./rules/legacy.ts"
22
22
 
@@ -110,7 +110,8 @@ const CHECK_SOURCES: Readonly<Record<ReviewCheckId, ReviewEvidenceSource>> = {
110
110
 
111
111
  const ALLOWED_FIXES = new Set<ReviewFixRecipeId>(["manifest.sync", "workspace-dist.rebuild"])
112
112
 
113
- const CHECK_CODES: Readonly<Record<string, ReviewCheckId>> = {
113
+ /** The review check each built-in code reports under; a code missing here would read as configuration. */
114
+ export const CHECK_CODES: Readonly<Record<string, ReviewCheckId>> = {
114
115
  "NF-C001": "typecheck",
115
116
  "NF-C002": "typed-client",
116
117
  "NF-C003": "typed-client",
@@ -134,18 +135,41 @@ const CHECK_CODES: Readonly<Record<string, ReviewCheckId>> = {
134
135
  "NF-C021": "route-boundary",
135
136
  "NF-C022": "route-boundary",
136
137
  "NF-C023": "route-boundary",
138
+ "NF-C024": "route-boundary",
139
+ "NF-C025": "route-boundary",
140
+ "NF-C026": "route-boundary",
141
+ "NF-C027": "route-boundary",
142
+ "NF-C028": "server-boundary",
143
+ "NF-C029": "security",
144
+ "NF-C030": "security",
145
+ "NF-C031": "security",
146
+ "NF-C032": "security",
147
+ "NF-C033": "security",
148
+ "NF-C034": "route-boundary",
149
+ "NF-C035": "route-boundary",
150
+ "NF-C036": "route-boundary",
151
+ "NF-C037": "configuration",
152
+ "NF-C038": "configuration",
153
+ "NF-C039": "contract-witness",
154
+ "NF-C040": "route-boundary",
137
155
  "NF-D001": "dependency",
156
+ "NF-D002": "dependency",
138
157
  "NF-A001": "route-assurance",
139
158
  "NF-H001": "hydration",
140
159
  "NF-H002": "hydration",
141
160
  "NF-H003": "hydration",
142
161
  "NF-H004": "hydration",
143
162
  "NF-K001": "contract-witness",
163
+ "NF-S001": "security",
164
+ "NF-S002": "security",
165
+ "NF-S003": "security",
166
+ "NF-S004": "security",
167
+ "NF-S005": "security",
168
+ "NF-S006": "security",
169
+ "NF-S007": "security",
170
+ "NF-S008": "security",
144
171
  }
145
172
 
146
- for (const code of ["NF-S001", "NF-S002", "NF-S003", "NF-S004", "NF-S005", "NF-S006", "NF-S007"])
147
- (CHECK_CODES as Record<string, ReviewCheckId>)[code] = "security"
148
-
149
173
  const REVIEW_INPUT_SCHEMA = {
150
174
  type: "object",
151
175
  properties: {
@@ -613,17 +637,22 @@ async function composeCollectedReport(
613
637
  }
614
638
 
615
639
  const changedPaths = new Set(scope.changedPaths)
640
+ // A server-only chain is in scope when any file on it changed: a new import in a shared module
641
+ // reaches the browser through a route the diff never touched.
642
+ const inScope = (candidate: CandidateFinding): boolean =>
643
+ candidate.location === undefined ||
644
+ changedPaths.has(candidate.location.path) ||
645
+ (diagnosticCompatibilityOf(candidate.diagnostic)?.modules ?? []).some((module) => {
646
+ const path = normalizedLocation(cwd, module, undefined)?.path
647
+ return path !== undefined && changedPaths.has(path)
648
+ })
616
649
  const retainedByCheck = new Map<ReviewCheckId, CandidateFinding[]>()
617
650
  let totalOutOfScope = 0
618
651
  let typeErrorOutOfScope = false
619
652
  for (const id of CHECK_IDS) {
620
653
  const retained: CandidateFinding[] = []
621
654
  for (const candidate of candidatesByCheck.get(id) ?? []) {
622
- if (
623
- scope.kind === "diff" &&
624
- candidate.location !== undefined &&
625
- !changedPaths.has(candidate.location.path)
626
- ) {
655
+ if (scope.kind === "diff" && !inScope(candidate)) {
627
656
  totalOutOfScope += 1
628
657
  if (id === "typecheck" && candidate.severity === "error") typeErrorOutOfScope = true
629
658
  } else retained.push(candidate)
@@ -29,11 +29,20 @@ export const RULE_CODES = Object.freeze({
29
29
  "NF-C030": "Route data without an output schema",
30
30
  "NF-C031": "Sensitive field in an output schema",
31
31
  "NF-C032": "Credential in browser code",
32
+ "NF-C033": "Generated output schema out of date",
33
+ "NF-C034": "Root status page wraps the root layout itself",
34
+ "NF-C035": "Backend mounted at the implicit /api",
35
+ "NF-C036": "Route path the static scan cannot read",
36
+ "NF-C037": "API-only declaration over a web app",
37
+ "NF-C038": "Retired root layout file",
38
+ "NF-C039": "Implemented contract operation the backend does not serve",
39
+ "NF-C040": "Layout meta reads data its layout never loads",
32
40
  "NF-C020": "Island enhancer cleanup check",
33
41
  "NF-C021": "nano binding cleanup check",
34
42
  "NF-C022": "nano bindList key check",
35
43
  "NF-C023": "nano computed deps check",
36
44
  "NF-D001": "Doctor check",
45
+ "NF-D002": "Mixed nifra versions",
37
46
  "NF-A001": "Assurance bundle check",
38
47
  "NF-H001": "Hydration mismatch",
39
48
  "NF-H002": "Duplicate framework runtime",
@@ -47,6 +56,7 @@ export const RULE_CODES = Object.freeze({
47
56
  "NF-S005": "External redirect opt-out",
48
57
  "NF-S006": "Security escape hatch enabled",
49
58
  "NF-S007": "Secure cookie without a __Host-/__Secure- prefix",
59
+ "NF-S008": "Authenticated route validates input before authentication",
50
60
  } as const)
51
61
 
52
62
  /** Codes that no longer fire but remain reserved forever. */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Marker recognition for in-source escape hatches (`@nifra-gate-reviewed`, `nifra-expect …`).
2
+ * Marker recognition for in-source escape hatches (`nifra-expect <code>: <reason>`).
3
3
  *
4
4
  * A marker counts when it appears on the flagged line itself (trailing comment) or ANYWHERE in the
5
5
  * contiguous comment block directly above it - which is where a human naturally writes the
@@ -11,12 +11,27 @@
11
11
  // open/body/close lines, and the JSX comment form.
12
12
  const COMMENT_LINE = /^\s*(?:\/\/|\/\*|\*\/|\*|\{\/\*)/
13
13
 
14
+ /** The trailing comment on a code line: from its first `//` or `/*` outside a string literal to the
15
+ * end, else `""`. A marker in a string the code passes is data, not a comment. */
16
+ function trailingComment(text: string): string {
17
+ let quote: string | undefined
18
+ for (let i = 0; i < text.length; i++) {
19
+ const char = text[i]
20
+ if (quote !== undefined) {
21
+ if (char === "\\") i++
22
+ else if (char === quote) quote = undefined
23
+ } else if (char === '"' || char === "'" || char === "`") quote = char
24
+ else if (char === "/" && (text[i + 1] === "/" || text[i + 1] === "*")) return text.slice(i)
25
+ }
26
+ return ""
27
+ }
28
+
14
29
  export function commentBlockHasMarker(
15
30
  lines: readonly string[],
16
31
  line: number,
17
32
  marker: string,
18
33
  ): boolean {
19
- if ((lines[line - 1] ?? "").includes(marker)) return true
34
+ if (trailingComment(lines[line - 1] ?? "").includes(marker)) return true
20
35
  for (let i = line - 2; i >= 0; i--) {
21
36
  const text = lines[i] ?? ""
22
37
  if (!COMMENT_LINE.test(text)) return false
@@ -26,37 +41,58 @@ export function commentBlockHasMarker(
26
41
  }
27
42
 
28
43
  /**
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.
44
+ * The 1-based line a marker comment on `line` applies to: `line` itself when code precedes the comment
45
+ * there, else the first line after its comment block, or `undefined` when a blank line ends the block.
34
46
  */
35
- export function commentBlockMarkerReason(
47
+ export function markerTarget(lines: readonly string[], line: number): number | undefined {
48
+ if (!COMMENT_LINE.test(lines[line - 1] ?? "")) return line
49
+ for (let i = line; i < lines.length; i++) {
50
+ const text = lines[i] ?? ""
51
+ if (COMMENT_LINE.test(text)) continue
52
+ return text.trim() === "" ? undefined : i + 1
53
+ }
54
+ return undefined
55
+ }
56
+
57
+ /** What a finding adds when a `nifra-expect` comment names it but leaves out the reason it needs. */
58
+ export const MISSING_EXPECTATION_REASON =
59
+ "the nifra-expect comment here gives no reason after its colon, so it does not silence this"
60
+
61
+ /**
62
+ * A `nifra-expect <name>: <reason>` comment for the finding on `line`, where `<name>` is one of
63
+ * `names`: the finding's NF code, or a slug it also answers to. It counts in the same trailing-comment /
64
+ * comment-block-above region as {@link commentBlockHasMarker}. `undefined` when no such comment is
65
+ * there; `reason` is `undefined` when none follows the name's colon. A hatch that silences an error
66
+ * requires the reason, so an empty `nifra-expect NF-C006:` never suppresses one.
67
+ */
68
+ export function commentBlockExpectation(
36
69
  lines: readonly string[],
37
70
  line: number,
38
- marker: string,
39
- ): string | undefined {
71
+ names: readonly string[],
72
+ ): { readonly reason: string | undefined } | undefined {
73
+ const escaped = names.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
74
+ const pattern = new RegExp(`nifra-expect\\s+(?:${escaped.join("|")})(?![\\w-])(.*)`)
75
+ let found = false
40
76
  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
77
+ const match = pattern.exec(text)
78
+ if (match === null) return undefined
79
+ found = true
80
+ const rest = (match[1] ?? "").trimStart()
81
+ if (!rest.startsWith(":")) return undefined
46
82
  // A block comment's `*/` closer is not part of the reason.
47
83
  const reason = rest
48
- .slice(colon + 1)
84
+ .slice(1)
49
85
  .replace(/\*\/\s*$/, "")
50
86
  .trim()
51
87
  return reason.length > 0 ? reason : undefined
52
88
  }
53
- const onLine = reasonOn(lines[line - 1] ?? "")
54
- if (onLine !== undefined) return onLine
89
+ const onLine = reasonOn(trailingComment(lines[line - 1] ?? ""))
90
+ if (onLine !== undefined) return { reason: onLine }
55
91
  for (let i = line - 2; i >= 0; i--) {
56
92
  const text = lines[i] ?? ""
57
- if (!COMMENT_LINE.test(text)) return undefined
93
+ if (!COMMENT_LINE.test(text)) break
58
94
  const reason = reasonOn(text)
59
- if (reason !== undefined) return reason
95
+ if (reason !== undefined) return { reason }
60
96
  }
61
- return undefined
97
+ return found ? { reason: undefined } : undefined
62
98
  }