@nifrajs/cli 4.0.2 → 5.1.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 (364) hide show
  1. package/README.md +10 -2
  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 +9 -2
  12. package/dist/assure-hydration.js.map +1 -1
  13. package/dist/assure.d.ts +12 -2
  14. package/dist/assure.d.ts.map +1 -1
  15. package/dist/assure.js +59 -8
  16. package/dist/assure.js.map +1 -1
  17. package/dist/capabilities-tool.d.ts.map +1 -1
  18. package/dist/capabilities-tool.js +36 -9
  19. package/dist/capabilities-tool.js.map +1 -1
  20. package/dist/check-diagnostics.d.ts +22 -2
  21. package/dist/check-diagnostics.d.ts.map +1 -1
  22. package/dist/check-diagnostics.js +54 -7
  23. package/dist/check-diagnostics.js.map +1 -1
  24. package/dist/check-scan.d.ts +83 -24
  25. package/dist/check-scan.d.ts.map +1 -1
  26. package/dist/check-scan.js +1516 -318
  27. package/dist/check-scan.js.map +1 -1
  28. package/dist/check.d.ts +2 -2
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +203 -70
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +9 -0
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +215 -50
  35. package/dist/cli.js.map +1 -1
  36. package/dist/client-module.d.ts +20 -0
  37. package/dist/client-module.d.ts.map +1 -0
  38. package/dist/client-module.js +73 -0
  39. package/dist/client-module.js.map +1 -0
  40. package/dist/command-catalog.d.ts +24 -5
  41. package/dist/command-catalog.d.ts.map +1 -1
  42. package/dist/command-catalog.js +194 -45
  43. package/dist/command-catalog.js.map +1 -1
  44. package/dist/component-check.d.ts +13 -0
  45. package/dist/component-check.d.ts.map +1 -0
  46. package/dist/component-check.js +59 -0
  47. package/dist/component-check.js.map +1 -0
  48. package/dist/contract-proof.d.ts +1 -1
  49. package/dist/contract-proof.d.ts.map +1 -1
  50. package/dist/contract-proof.js +2 -24
  51. package/dist/contract-proof.js.map +1 -1
  52. package/dist/contracts.d.ts +19 -0
  53. package/dist/contracts.d.ts.map +1 -1
  54. package/dist/contracts.js +50 -3
  55. package/dist/contracts.js.map +1 -1
  56. package/dist/db-tool.d.ts.map +1 -1
  57. package/dist/db-tool.js +4 -3
  58. package/dist/db-tool.js.map +1 -1
  59. package/dist/dev-bun-config.d.ts +5 -1
  60. package/dist/dev-bun-config.d.ts.map +1 -1
  61. package/dist/dev-bun-config.js +44 -11
  62. package/dist/dev-bun-config.js.map +1 -1
  63. package/dist/dev-feed-tool.d.ts +18 -1
  64. package/dist/dev-feed-tool.d.ts.map +1 -1
  65. package/dist/dev-feed-tool.js +93 -0
  66. package/dist/dev-feed-tool.js.map +1 -1
  67. package/dist/diagnostics.d.ts +3 -0
  68. package/dist/diagnostics.d.ts.map +1 -1
  69. package/dist/diagnostics.js.map +1 -1
  70. package/dist/diff-tool.d.ts +9 -8
  71. package/dist/diff-tool.d.ts.map +1 -1
  72. package/dist/diff-tool.js +28 -24
  73. package/dist/diff-tool.js.map +1 -1
  74. package/dist/doctor.d.ts +13 -0
  75. package/dist/doctor.d.ts.map +1 -1
  76. package/dist/doctor.js +23 -15
  77. package/dist/doctor.js.map +1 -1
  78. package/dist/env-file.d.ts +6 -0
  79. package/dist/env-file.d.ts.map +1 -1
  80. package/dist/env-file.js +6 -0
  81. package/dist/env-file.js.map +1 -1
  82. package/dist/fix-recipes.d.ts +2 -0
  83. package/dist/fix-recipes.d.ts.map +1 -1
  84. package/dist/fix-recipes.js +102 -2
  85. package/dist/fix-recipes.js.map +1 -1
  86. package/dist/hand-built-manifest.d.ts +27 -0
  87. package/dist/hand-built-manifest.d.ts.map +1 -0
  88. package/dist/hand-built-manifest.js +124 -0
  89. package/dist/hand-built-manifest.js.map +1 -0
  90. package/dist/hooks-report.d.ts +23 -0
  91. package/dist/hooks-report.d.ts.map +1 -0
  92. package/dist/hooks-report.js +104 -0
  93. package/dist/hooks-report.js.map +1 -0
  94. package/dist/init-agents.d.ts +18 -2
  95. package/dist/init-agents.d.ts.map +1 -1
  96. package/dist/init-agents.js +20 -13
  97. package/dist/init-agents.js.map +1 -1
  98. package/dist/internal/config-paths.d.ts +16 -0
  99. package/dist/internal/config-paths.d.ts.map +1 -0
  100. package/dist/internal/config-paths.js +124 -0
  101. package/dist/internal/config-paths.js.map +1 -0
  102. package/dist/internal/installed-package.d.ts +4 -0
  103. package/dist/internal/installed-package.d.ts.map +1 -0
  104. package/dist/internal/installed-package.js +17 -0
  105. package/dist/internal/installed-package.js.map +1 -0
  106. package/dist/internal/nifra-project.d.ts +41 -0
  107. package/dist/internal/nifra-project.d.ts.map +1 -0
  108. package/dist/internal/nifra-project.js +183 -0
  109. package/dist/internal/nifra-project.js.map +1 -0
  110. package/dist/internal/project-specifiers.d.ts +29 -0
  111. package/dist/internal/project-specifiers.d.ts.map +1 -0
  112. package/dist/internal/project-specifiers.js +435 -0
  113. package/dist/internal/project-specifiers.js.map +1 -0
  114. package/dist/internal/script-kind.d.ts.map +1 -1
  115. package/dist/internal/script-kind.js +2 -1
  116. package/dist/internal/script-kind.js.map +1 -1
  117. package/dist/internal/source-facts.d.ts +8 -1
  118. package/dist/internal/source-facts.d.ts.map +1 -1
  119. package/dist/internal/source-facts.js +22 -5
  120. package/dist/internal/source-facts.js.map +1 -1
  121. package/dist/internal/typescript-import.d.ts +1 -1
  122. package/dist/internal/typescript-import.d.ts.map +1 -1
  123. package/dist/internal/typescript-import.js +10 -6
  124. package/dist/internal/typescript-import.js.map +1 -1
  125. package/dist/introspect.d.ts +15 -6
  126. package/dist/introspect.d.ts.map +1 -1
  127. package/dist/introspect.js +87 -25
  128. package/dist/introspect.js.map +1 -1
  129. package/dist/levels-tool.d.ts.map +1 -1
  130. package/dist/levels-tool.js +98 -23
  131. package/dist/levels-tool.js.map +1 -1
  132. package/dist/load.d.ts +33 -10
  133. package/dist/load.d.ts.map +1 -1
  134. package/dist/load.js +60 -10
  135. package/dist/load.js.map +1 -1
  136. package/dist/mcp-context.d.ts +5 -5
  137. package/dist/mcp-context.d.ts.map +1 -1
  138. package/dist/mcp-context.js +8 -23
  139. package/dist/mcp-context.js.map +1 -1
  140. package/dist/mcp-delegate.d.ts +2 -2
  141. package/dist/mcp-delegate.d.ts.map +1 -1
  142. package/dist/mcp-delegate.js +19 -7
  143. package/dist/mcp-delegate.js.map +1 -1
  144. package/dist/mcp-exec.d.ts +13 -4
  145. package/dist/mcp-exec.d.ts.map +1 -1
  146. package/dist/mcp-exec.js +56 -14
  147. package/dist/mcp-exec.js.map +1 -1
  148. package/dist/mcp-http.js +1 -1
  149. package/dist/mcp-io.d.ts +30 -0
  150. package/dist/mcp-io.d.ts.map +1 -1
  151. package/dist/mcp-io.js +63 -0
  152. package/dist/mcp-io.js.map +1 -1
  153. package/dist/mcp-isolate.d.ts.map +1 -1
  154. package/dist/mcp-isolate.js +3 -2
  155. package/dist/mcp-isolate.js.map +1 -1
  156. package/dist/mcp-path.d.ts +2 -1
  157. package/dist/mcp-path.d.ts.map +1 -1
  158. package/dist/mcp-path.js +9 -24
  159. package/dist/mcp-path.js.map +1 -1
  160. package/dist/mcp-project-child.js +1 -1
  161. package/dist/mcp-project-child.js.map +1 -1
  162. package/dist/mcp-root.d.ts.map +1 -1
  163. package/dist/mcp-root.js +50 -10
  164. package/dist/mcp-root.js.map +1 -1
  165. package/dist/mcp-ws.d.ts.map +1 -1
  166. package/dist/mcp-ws.js +3 -1
  167. package/dist/mcp-ws.js.map +1 -1
  168. package/dist/mcp.d.ts.map +1 -1
  169. package/dist/mcp.js +48 -19
  170. package/dist/mcp.js.map +1 -1
  171. package/dist/migrate-layout.d.ts +47 -1
  172. package/dist/migrate-layout.d.ts.map +1 -1
  173. package/dist/migrate-layout.js +1972 -170
  174. package/dist/migrate-layout.js.map +1 -1
  175. package/dist/output-schemas.d.ts +128 -0
  176. package/dist/output-schemas.d.ts.map +1 -0
  177. package/dist/output-schemas.js +975 -0
  178. package/dist/output-schemas.js.map +1 -0
  179. package/dist/port.d.ts.map +1 -1
  180. package/dist/port.js +3 -2
  181. package/dist/port.js.map +1 -1
  182. package/dist/project-facts.d.ts +15 -4
  183. package/dist/project-facts.d.ts.map +1 -1
  184. package/dist/project-facts.js +1 -0
  185. package/dist/project-facts.js.map +1 -1
  186. package/dist/project-path.d.ts +6 -0
  187. package/dist/project-path.d.ts.map +1 -1
  188. package/dist/project-path.js +48 -14
  189. package/dist/project-path.js.map +1 -1
  190. package/dist/recipes/5.0.0.d.ts +11 -0
  191. package/dist/recipes/5.0.0.d.ts.map +1 -0
  192. package/dist/recipes/5.0.0.js +34 -0
  193. package/dist/recipes/5.0.0.js.map +1 -0
  194. package/dist/recipes/index.d.ts +7 -0
  195. package/dist/recipes/index.d.ts.map +1 -1
  196. package/dist/recipes/index.js +2 -0
  197. package/dist/recipes/index.js.map +1 -1
  198. package/dist/release-verification.d.ts +5 -0
  199. package/dist/release-verification.d.ts.map +1 -1
  200. package/dist/release-verification.js +14 -0
  201. package/dist/release-verification.js.map +1 -1
  202. package/dist/replay.d.ts.map +1 -1
  203. package/dist/replay.js +12 -3
  204. package/dist/replay.js.map +1 -1
  205. package/dist/review.d.ts +3 -1
  206. package/dist/review.d.ts.map +1 -1
  207. package/dist/review.js +43 -7
  208. package/dist/review.js.map +1 -1
  209. package/dist/rules/codes.d.ts +12 -0
  210. package/dist/rules/codes.d.ts.map +1 -1
  211. package/dist/rules/codes.js +12 -0
  212. package/dist/rules/codes.js.map +1 -1
  213. package/dist/rules/comment-markers.d.ts +16 -7
  214. package/dist/rules/comment-markers.d.ts.map +1 -1
  215. package/dist/rules/comment-markers.js +58 -19
  216. package/dist/rules/comment-markers.js.map +1 -1
  217. package/dist/rules/contract-visibility.d.ts +3 -0
  218. package/dist/rules/contract-visibility.d.ts.map +1 -0
  219. package/dist/rules/contract-visibility.js +337 -0
  220. package/dist/rules/contract-visibility.js.map +1 -0
  221. package/dist/rules/data-guard.d.ts +13 -0
  222. package/dist/rules/data-guard.d.ts.map +1 -1
  223. package/dist/rules/data-guard.js +356 -37
  224. package/dist/rules/data-guard.js.map +1 -1
  225. package/dist/rules/layout.d.ts +4 -0
  226. package/dist/rules/layout.d.ts.map +1 -0
  227. package/dist/rules/layout.js +19 -0
  228. package/dist/rules/layout.js.map +1 -0
  229. package/dist/rules/legacy.d.ts.map +1 -1
  230. package/dist/rules/legacy.js +178 -44
  231. package/dist/rules/legacy.js.map +1 -1
  232. package/dist/rules/pages.d.ts +50 -0
  233. package/dist/rules/pages.d.ts.map +1 -1
  234. package/dist/rules/pages.js +436 -4
  235. package/dist/rules/pages.js.map +1 -1
  236. package/dist/rules/routes.d.ts +6 -0
  237. package/dist/rules/routes.d.ts.map +1 -1
  238. package/dist/rules/routes.js +111 -16
  239. package/dist/rules/routes.js.map +1 -1
  240. package/dist/rules/secrets.js +2 -2
  241. package/dist/rules/secrets.js.map +1 -1
  242. package/dist/rules/security.d.ts +13 -1
  243. package/dist/rules/security.d.ts.map +1 -1
  244. package/dist/rules/security.js +313 -69
  245. package/dist/rules/security.js.map +1 -1
  246. package/dist/rules/zones.d.ts +13 -0
  247. package/dist/rules/zones.d.ts.map +1 -1
  248. package/dist/rules/zones.js +218 -44
  249. package/dist/rules/zones.js.map +1 -1
  250. package/dist/scaffold.d.ts.map +1 -1
  251. package/dist/scaffold.js +6 -4
  252. package/dist/scaffold.js.map +1 -1
  253. package/dist/sdk.d.ts.map +1 -1
  254. package/dist/sdk.js +14 -5
  255. package/dist/sdk.js.map +1 -1
  256. package/dist/stylex-migrate.d.ts.map +1 -1
  257. package/dist/stylex-migrate.js +290 -24
  258. package/dist/stylex-migrate.js.map +1 -1
  259. package/dist/test-tool.d.ts.map +1 -1
  260. package/dist/test-tool.js +6 -3
  261. package/dist/test-tool.js.map +1 -1
  262. package/dist/typecheck-coverage.d.ts +20 -0
  263. package/dist/typecheck-coverage.d.ts.map +1 -0
  264. package/dist/typecheck-coverage.js +129 -0
  265. package/dist/typecheck-coverage.js.map +1 -0
  266. package/dist/upgrade.d.ts +84 -0
  267. package/dist/upgrade.d.ts.map +1 -1
  268. package/dist/upgrade.js +498 -78
  269. package/dist/upgrade.js.map +1 -1
  270. package/dist/verification-plan.d.ts.map +1 -1
  271. package/dist/verification-plan.js +16 -2
  272. package/dist/verification-plan.js.map +1 -1
  273. package/dist/version-drift.d.ts +22 -0
  274. package/dist/version-drift.d.ts.map +1 -0
  275. package/dist/version-drift.js +94 -0
  276. package/dist/version-drift.js.map +1 -0
  277. package/dist/web-app-options.d.ts +1 -1
  278. package/dist/web-app-options.d.ts.map +1 -1
  279. package/dist/web-app-options.js +1 -1
  280. package/dist/web-app-options.js.map +1 -1
  281. package/dist/work-graph.d.ts.map +1 -1
  282. package/dist/work-graph.js +20 -8
  283. package/dist/work-graph.js.map +1 -1
  284. package/docs/examples.json +55 -7
  285. package/docs/llms-full.txt +2043 -222
  286. package/docs/types.json +1573 -246
  287. package/package.json +11 -12
  288. package/src/agent-files.ts +203 -0
  289. package/src/app-files.ts +34 -4
  290. package/src/assure-hydration.ts +18 -2
  291. package/src/assure.ts +88 -10
  292. package/src/capabilities-tool.ts +47 -9
  293. package/src/check-diagnostics.ts +75 -9
  294. package/src/check-scan.ts +1816 -377
  295. package/src/check.ts +258 -81
  296. package/src/cli.ts +242 -37
  297. package/src/client-module.ts +97 -0
  298. package/src/command-catalog.ts +237 -52
  299. package/src/component-check.ts +79 -0
  300. package/src/contract-proof.ts +3 -25
  301. package/src/contracts.ts +60 -3
  302. package/src/db-tool.ts +5 -2
  303. package/src/dev-bun-config.ts +51 -10
  304. package/src/dev-feed-tool.ts +112 -1
  305. package/src/diagnostics.ts +3 -0
  306. package/src/diff-tool.ts +33 -26
  307. package/src/doctor.ts +43 -21
  308. package/src/env-file.ts +7 -0
  309. package/src/fix-recipes.ts +116 -2
  310. package/src/hand-built-manifest.ts +144 -0
  311. package/src/hooks-report.ts +135 -0
  312. package/src/init-agents.ts +21 -14
  313. package/src/internal/config-paths.ts +154 -0
  314. package/src/internal/installed-package.ts +15 -0
  315. package/src/internal/nifra-project.ts +191 -0
  316. package/src/internal/project-specifiers.ts +477 -0
  317. package/src/internal/script-kind.ts +2 -1
  318. package/src/internal/source-facts.ts +48 -5
  319. package/src/internal/typescript-import.ts +15 -6
  320. package/src/introspect.ts +104 -28
  321. package/src/levels-tool.ts +124 -29
  322. package/src/load.ts +107 -17
  323. package/src/mcp-context.ts +12 -25
  324. package/src/mcp-delegate.ts +22 -10
  325. package/src/mcp-exec.ts +68 -18
  326. package/src/mcp-http.ts +1 -1
  327. package/src/mcp-io.ts +74 -0
  328. package/src/mcp-isolate.ts +9 -2
  329. package/src/mcp-path.ts +8 -22
  330. package/src/mcp-project-child.ts +1 -1
  331. package/src/mcp-root.ts +58 -8
  332. package/src/mcp-ws.ts +4 -2
  333. package/src/mcp.ts +56 -22
  334. package/src/migrate-layout.ts +2257 -176
  335. package/src/output-schemas.ts +1240 -0
  336. package/src/port.ts +3 -2
  337. package/src/project-facts.ts +17 -3
  338. package/src/project-path.ts +44 -13
  339. package/src/recipes/5.0.0.ts +35 -0
  340. package/src/recipes/index.ts +10 -0
  341. package/src/release-verification.ts +20 -0
  342. package/src/replay.ts +14 -3
  343. package/src/review.ts +44 -10
  344. package/src/rules/codes.ts +12 -0
  345. package/src/rules/comment-markers.ts +57 -21
  346. package/src/rules/contract-visibility.ts +383 -0
  347. package/src/rules/data-guard.ts +391 -42
  348. package/src/rules/layout.ts +22 -0
  349. package/src/rules/legacy.ts +206 -52
  350. package/src/rules/pages.ts +481 -4
  351. package/src/rules/routes.ts +124 -16
  352. package/src/rules/secrets.ts +2 -2
  353. package/src/rules/security.ts +391 -72
  354. package/src/rules/zones.ts +232 -42
  355. package/src/scaffold.ts +6 -4
  356. package/src/sdk.ts +14 -5
  357. package/src/stylex-migrate.ts +251 -24
  358. package/src/test-tool.ts +12 -3
  359. package/src/typecheck-coverage.ts +134 -0
  360. package/src/upgrade.ts +576 -72
  361. package/src/verification-plan.ts +33 -2
  362. package/src/version-drift.ts +118 -0
  363. package/src/web-app-options.ts +2 -2
  364. package/src/work-graph.ts +17 -12
@@ -5,7 +5,7 @@ import type { AssuranceConfig, AssuranceReport } from "@nifrajs/core/assurance"
5
5
  import { type ProjectEvidenceSnapshot, snapshotProjectEvidence } from "@nifrajs/core/evidence"
6
6
  import { SINGLE_COPY_REGISTER_SPECIFIER } from "@nifrajs/core/single-copy"
7
7
  import type { CapabilityProjectReport } from "../capabilities-tool.ts"
8
- import type { SourceFinding, StaticRouteFinding } from "../check-scan.ts"
8
+ import type { SourceFinding, StaticRouteFinding, TypedClientFinding } from "../check-scan.ts"
9
9
  import {
10
10
  IDENT,
11
11
  movedExportSites,
@@ -22,7 +22,11 @@ import {
22
22
  } from "../diagnostics.ts"
23
23
  import type { DuplicateInstallFinding } from "../doctor.ts"
24
24
  import { codeUnitOrder } from "../internal/code-unit-order.ts"
25
+ import { createWebAppProbe, withApiKindHint } from "../internal/nifra-project.ts"
25
26
  import { unsupportedTypeScriptDiagnostic } from "../internal/typescript-import.ts"
27
+ import { zoneFolderOf } from "../typecheck-coverage.ts"
28
+ import { versionDriftMessage } from "../version-drift.ts"
29
+ import { MISSING_EXPECTATION_REASON } from "./comment-markers.ts"
26
30
  import type { CheckRule, RuleContext } from "./index.ts"
27
31
 
28
32
  /** Legacy rule name → stable NF- code. Exported so `nifra.check.json` rule overrides can be keyed
@@ -89,13 +93,13 @@ export const LEGACY_RULE_ORDER: readonly string[] = Object.freeze([
89
93
  ])
90
94
 
91
95
  const FETCH_HINT =
92
- "hand-rolled fetch() to your own API - call it through client<typeof app> (from @nifrajs/client) so the compiler catches drift"
96
+ "hand-rolled fetch() to your own API - call it through client<typeof app> (from @nifrajs/client) so the compiler catches drift; for a call no typed client covers (a foreign handler, a static file), add a `// nifra-expect NF-C002: <reason>` comment"
93
97
  const STREAM_HINT =
94
- "hand-rolled EventSource/WebSocket to your own API - subscribe through client<typeof app> (`.subscribe()` for `app.sse()` routes, `.ws()` for `app.ws()` routes) so the compiler catches drift"
98
+ "hand-rolled EventSource/WebSocket to your own API - subscribe through client<typeof app> (`.subscribe()` for `app.sse()` routes, `.ws()` for `app.ws()` routes) so the compiler catches drift; for a stream no typed client covers, add a `// nifra-expect NF-C002: <reason>` comment"
95
99
  const SERVER_IMPORT_HINT =
96
100
  "server-only code in browser code (a route's frontend half, frontend/ or shared/) - reach it from the route's backend half (x.backend.ts) or a *.fn.ts server function; a dynamic import() is bundled too"
97
101
  const RESPONSE_ROUTE_HINT =
98
- "route handler returns a raw Response - the typed client infers `data: never`, so drift detection is lost for this route. Return a plain object (it's serialized for you); for a stream use a typed SSE route (`app.sse(...)`), which keeps typed events; or, if a raw Response is intended (file/redirect), add `{ response: t.… }` or a `// nifra-expect raw-response` comment to mark it and silence this"
102
+ "route handler returns a raw Response - the typed client infers `data: never`, so drift detection is lost for this route. Return a plain object (it's serialized for you); for a stream use a typed SSE route (`app.sse(...)`), which keeps typed events; or, if a raw Response is intended (file/redirect), add `{ response: t.… }` or a `// nifra-expect NF-C007: <reason>` comment to mark it and silence this"
99
103
  const PIPELINE_DOC_HINT =
100
104
  "nifra runs one bundler per phase - `vitePlugins` feed Vite, `clientPlugins`/`serverPlugins` feed Bun, and the file `nifra build` imports the adapter from is bundled into the server. See the Gotchas section of the Dev & HMR guide."
101
105
  const UNDECLARED_DEP_HINT =
@@ -124,6 +128,7 @@ interface LegacyFields {
124
128
  readonly message: string
125
129
  readonly evidence?: readonly string[]
126
130
  readonly chain?: readonly string[]
131
+ readonly modules?: readonly string[]
127
132
  readonly fix?: string
128
133
  /** A recipe for this one finding, when the rule's findings are not all mechanically fixable. */
129
134
  readonly recipe?: Diagnostic["fix"]
@@ -163,6 +168,7 @@ function legacyDiagnostic(rule: string, fields: LegacyFields): Diagnostic {
163
168
  ...(fields.fix === undefined ? {} : { fix: fields.fix }),
164
169
  ...(fields.suggestion === undefined ? {} : { suggestion: fields.suggestion }),
165
170
  ...(fields.chain === undefined ? {} : { chain: fields.chain }),
171
+ ...(fields.modules === undefined ? {} : { modules: fields.modules }),
166
172
  ...(fields.includeCode === true ? { includeCode: true } : {}),
167
173
  }
168
174
  return diagnosticWithCompatibility(canonical, compatibility)
@@ -312,7 +318,7 @@ function responseRouteSuggestion(): DiagnosticSuggestion {
312
318
  steps: [
313
319
  "Prefer returning a plain object from JSON routes; nifra serializes it for you.",
314
320
  "For a stream, use a typed SSE route - `app.sse(...)` (or `sse(c, run)` from `@nifrajs/core/server`) - which keeps typed events instead of collapsing the client to `data: never`.",
315
- "If this route must return a raw Response (redirect, file), declare an explicit response schema, or add a `// nifra-expect raw-response` comment above the return to mark it intentional and silence this advisory.",
321
+ "If this route must return a raw Response (redirect, file), declare an explicit response schema, or add a `// nifra-expect NF-C007: <reason>` comment above the return to mark it intentional and silence this advisory.",
316
322
  ],
317
323
  }
318
324
  }
@@ -339,6 +345,25 @@ const typecheckRule: CheckRule = {
339
345
  }),
340
346
  )
341
347
  }
348
+ if (tc.sourcesWithoutTsconfig !== undefined) {
349
+ findings.push(
350
+ legacyDiagnostic("typecheck", {
351
+ severity: "error",
352
+ file: "tsconfig.json",
353
+ message: `${tc.sourcesWithoutTsconfig} TypeScript file(s) but no tsconfig.json in this directory - the typecheck gate did NOT run`,
354
+ fix: "add a tsconfig.json that includes the project's sources",
355
+ suggestion: {
356
+ kind: "manual",
357
+ title: "Add a tsconfig.json so the contract gate can run",
358
+ steps: [
359
+ "Create tsconfig.json in the directory `nifra check` runs from, with `include` covering the project's TypeScript sources.",
360
+ "In a monorepo, run `nifra check` from each app that has its own tsconfig.json.",
361
+ "Run `nifra_check` again: the typecheck now runs `tsc --noEmit -p tsconfig.json`.",
362
+ ],
363
+ },
364
+ }),
365
+ )
366
+ }
342
367
  if (tc.ran && !tc.ok) {
343
368
  const lines = (tc.output ?? "").split("\n")
344
369
  let matched = false
@@ -378,35 +403,93 @@ const typecheckRule: CheckRule = {
378
403
  )
379
404
  }
380
405
  }
406
+ findings.push(...outsideProgramFindings(tc.outsideProgram ?? []))
381
407
  return findings
382
408
  },
383
409
  }
384
410
 
411
+ /** One finding per zone folder: the fix (tsconfig `include`) is per folder, not per file. */
412
+ function outsideProgramFindings(files: readonly string[]): Diagnostic[] {
413
+ const byFolder = new Map<string, { readonly first: string; readonly group: string[] }>()
414
+ for (const file of files) {
415
+ const folder = zoneFolderOf(file) ?? file
416
+ const entry = byFolder.get(folder)
417
+ if (entry === undefined) byFolder.set(folder, { first: file, group: [file] })
418
+ else entry.group.push(file)
419
+ }
420
+ return [...byFolder].map(([folder, { first, group }]) => {
421
+ const shown = group.slice(0, 3).join(", ")
422
+ const more = group.length > 3 ? ` and ${group.length - 3} more` : ""
423
+ return legacyDiagnostic("typecheck", {
424
+ severity: "error",
425
+ file: first,
426
+ message: `${group.length} file(s) under ${folder}/ are outside tsc's program, so the typecheck passed without reading them: ${shown}${more}`,
427
+ evidence: group,
428
+ fix: `Add "${folder}" to tsconfig.json "include" (or list the files), then run \`nifra check\` again`,
429
+ suggestion: {
430
+ kind: "manual",
431
+ title: "Bring the folder into the TypeScript program",
432
+ steps: [
433
+ `Add "${folder}" to the "include" array in tsconfig.json.`,
434
+ 'Files kept out on purpose: name them in the tsconfig "exclude", or under "typecheck": { "ignore": [...] } in nifra.check.json.',
435
+ "Run `nifra check` again; the newly checked files may report type errors.",
436
+ ],
437
+ },
438
+ })
439
+ })
440
+ }
441
+
442
+ /**
443
+ * One NF-C002 finding. A `// nifra-expect NF-C002: <reason>` comment keeps it in the report as `info`,
444
+ * carrying the comment as evidence; the same comment without a reason silences nothing.
445
+ */
446
+ function typedClientDiagnostic(
447
+ finding: TypedClientFinding,
448
+ hint: string,
449
+ kind: string,
450
+ suggestion: DiagnosticSuggestion,
451
+ ): Diagnostic {
452
+ const reason = finding.expected?.reason
453
+ if (reason !== undefined) {
454
+ return legacyDiagnostic("typed-client", {
455
+ severity: "info",
456
+ file: finding.file,
457
+ line: finding.line,
458
+ message: `${finding.snippet} - ${kind} is expected here`,
459
+ evidence: [`nifra-expect NF-C002: ${reason}`],
460
+ })
461
+ }
462
+ const unreasoned = finding.expected === undefined ? "" : `; ${MISSING_EXPECTATION_REASON}`
463
+ return legacyDiagnostic("typed-client", {
464
+ severity: "error",
465
+ file: finding.file,
466
+ line: finding.line,
467
+ message: `${finding.snippet} - ${hint}${unreasoned}`,
468
+ fix: hint,
469
+ suggestion,
470
+ })
471
+ }
472
+
385
473
  const typedClientRule: CheckRule = {
386
474
  code: LEGACY_RULE_CODES["typed-client"]!,
387
475
  title: TITLES["typed-client"]!,
388
476
  async scan(ctx) {
389
477
  const routes = staticRouteMap(ctx.project.routes)
390
- const fetchFindings = [...ctx.project.sourceFindings.fetches].sort(bySite).map((finding) =>
391
- legacyDiagnostic("typed-client", {
392
- severity: "error",
393
- file: finding.file,
394
- line: finding.line,
395
- message: `${finding.snippet} - ${FETCH_HINT}`,
396
- fix: FETCH_HINT,
397
- suggestion: ownFetchSuggestion(finding, routes),
398
- }),
399
- )
400
- const streamFindings = [...ctx.project.sourceFindings.streams].sort(bySite).map((finding) =>
401
- legacyDiagnostic("typed-client", {
402
- severity: "error",
403
- file: finding.file,
404
- line: finding.line,
405
- message: `${finding.snippet} - ${STREAM_HINT}`,
406
- fix: STREAM_HINT,
407
- suggestion: ownStreamSuggestion(),
408
- }),
409
- )
478
+ const fetchFindings = [...ctx.project.sourceFindings.fetches]
479
+ .sort(bySite)
480
+ .map((finding) =>
481
+ typedClientDiagnostic(
482
+ finding,
483
+ FETCH_HINT,
484
+ "own-API fetch",
485
+ ownFetchSuggestion(finding, routes),
486
+ ),
487
+ )
488
+ const streamFindings = [...ctx.project.sourceFindings.streams]
489
+ .sort(bySite)
490
+ .map((finding) =>
491
+ typedClientDiagnostic(finding, STREAM_HINT, "own-API stream", ownStreamSuggestion()),
492
+ )
410
493
  return [...fetchFindings, ...streamFindings].sort(
411
494
  (a, b) => codeUnitOrder(a.file ?? "", b.file ?? "") || (a.line ?? 0) - (b.line ?? 0),
412
495
  )
@@ -468,19 +551,31 @@ const serverOnlyImportRule: CheckRule = {
468
551
  code: LEGACY_RULE_CODES["server-only-import"]!,
469
552
  title: TITLES["server-only-import"]!,
470
553
  async scan(ctx) {
471
- return [...ctx.project.importGraph].sort(bySite).map((finding) => {
554
+ const inWebApp = createWebAppProbe(ctx.root)
555
+ const findings: Diagnostic[] = []
556
+ // No component module got a receipt, so each is judged as an ordinary import: say why.
557
+ const components = ctx.project.check.componentCheckUnsupported
558
+ if (components !== undefined)
559
+ findings.push(unsupportedTypeScriptDiagnostic(components.version, components.message))
560
+ for (const finding of [...ctx.project.importGraph].sort(bySite)) {
472
561
  const chain = finding.chain
473
562
  const sink = chain[chain.length - 1] ?? finding.specifier
474
- return legacyDiagnostic("server-only-import", {
563
+ const dir = finding.file.slice(0, Math.max(finding.file.lastIndexOf("/"), 0))
564
+ const found = legacyDiagnostic("server-only-import", {
475
565
  severity: "error",
476
566
  file: finding.file,
477
567
  line: finding.line,
478
- message: `${finding.snippet} - server-only "${sink}" reaches the browser bundle via ${chain.join(" → ")}${finding.fallback ? " (direct edge - couldn't resolve the transitive chain precisely)" : ""}; ${SERVER_IMPORT_HINT}`,
568
+ message: (inWebApp(dir) ? (text: string) => text : withApiKindHint)(
569
+ `${finding.snippet} - server-only "${sink}" reaches the browser bundle via ${chain.join(" → ")}${finding.fallback ? " (direct edge - couldn't resolve the transitive chain precisely)" : ""}; ${SERVER_IMPORT_HINT}`,
570
+ ),
479
571
  fix: SERVER_IMPORT_HINT,
480
572
  chain,
573
+ modules: finding.modules,
481
574
  suggestion: serverImportSuggestion(finding.specifier, chain, finding.fallback),
482
575
  })
483
- })
576
+ findings.push(found)
577
+ }
578
+ return findings
484
579
  },
485
580
  }
486
581
 
@@ -507,6 +602,18 @@ const interpolatedSqlRule: CheckRule = {
507
602
  )
508
603
  }
509
604
  for (const finding of [...ctx.project.sourceFindings.interpolatedSql].sort(bySite)) {
605
+ if (finding.expected !== undefined) {
606
+ findings.push(
607
+ legacyDiagnostic("interpolated-sql", {
608
+ severity: "info",
609
+ file: finding.file,
610
+ line: finding.line,
611
+ message: `${finding.snippet} - interpolated SQL is expected here`,
612
+ evidence: [`nifra-expect NF-C006: ${finding.expected}`],
613
+ }),
614
+ )
615
+ continue
616
+ }
510
617
  findings.push(
511
618
  legacyDiagnostic("interpolated-sql", {
512
619
  severity: "error",
@@ -522,7 +629,7 @@ const interpolatedSqlRule: CheckRule = {
522
629
  "Pass the value as an argument alongside the statement, so the driver binds it.",
523
630
  `Or switch to the driver's tagged template (sql\`… ${SQL_INTERPOLATION_EXAMPLE} …\`): the tag receives substitutions separately and binds them.`,
524
631
  "An identifier that genuinely cannot be bound (a table or column name) must be checked against an allowlist you control, never taken from the request.",
525
- "If the statement is dynamic but every value is already bound (generating `($1),($2),…` for a batch insert), mark it with a `// nifra-expect sql-dynamic: <reason>` comment on the line above - the reason is required.",
632
+ "If the statement is dynamic but every value is already bound (generating `($1),($2),…` for a batch insert), mark it with a `// nifra-expect NF-C006: <reason>` comment on the line above - the reason is required.",
526
633
  ],
527
634
  },
528
635
  }),
@@ -556,31 +663,61 @@ function copyLines(finding: DuplicateInstallFinding): string[] {
556
663
  )
557
664
  }
558
665
 
559
- function duplicateSuggestion(finding: DuplicateInstallFinding): DiagnosticSuggestion {
560
- const lines = copyLines(finding)
666
+ /** One fix for duplicates that share a cause and topology, so a linked sibling repository that
667
+ * duplicates every package reads as one finding instead of one paragraph per package. */
668
+ function duplicateSuggestion(group: readonly DuplicateInstallFinding[]): DiagnosticSuggestion {
669
+ const names = group.map((finding) => `"${finding.package}"`).join(", ")
561
670
  return {
562
671
  kind: "manual",
563
- title: `Resolve ${finding.package} to one loaded copy`,
672
+ title:
673
+ group.length === 1
674
+ ? `Resolve ${group[0]?.package} to one loaded copy`
675
+ : `Resolve each of ${group.length} packages to one loaded copy`,
564
676
  diff: [
565
677
  "package.json:",
566
- ` "nifra": { "singleCopy": ["${finding.package}"] }`,
678
+ ` "nifra": { "singleCopy": [${names}] }`,
567
679
  "bunfig.toml:",
568
680
  ` preload = ["${SINGLE_COPY_REGISTER_SPECIFIER}"]`,
569
681
  " [test]",
570
682
  ` preload = ["${SINGLE_COPY_REGISTER_SPECIFIER}"]`,
571
683
  ].join("\n"),
572
684
  steps: [
573
- ...lines.map((line) => `Copy: ${line}`),
574
- ...(finding.provenance === undefined
575
- ? []
576
- : [`Planted links: ${finding.provenance} Try this before the fixes below.`]),
685
+ ...group.flatMap(copyLines).map((line) => `Copy: ${line}`),
686
+ ...group.flatMap((finding) =>
687
+ finding.provenance === undefined
688
+ ? []
689
+ : [
690
+ `Planted links${group.length === 1 ? "" : ` (${finding.package})`}: ${finding.provenance} Try this before the fixes below.`,
691
+ ],
692
+ ),
577
693
  "Fix 1 - deduplicate: align workspace dependency and peer ranges on one compatible version, remove stale nested installs, and reinstall from the workspace root so every importer resolves one physical copy.",
578
- `Fix 2 - declare single-copy: apply the package.json and bunfig.toml config printed above; nifra then rewrites every duplicate to this app's copy.${finding.cause === "version-skew" ? " A declaration only covers same-version duplicates, so fix 1's range alignment must land first." : ""}`,
694
+ `Fix 2 - declare single-copy: apply the package.json and bunfig.toml config printed above; nifra then rewrites every duplicate to this app's copy.${group.some((finding) => finding.cause === "version-skew") ? " A declaration only covers same-version duplicates, so fix 1's range alignment must land first." : ""}`,
579
695
  "Re-run `nifra check`; the gate stays failing until one fix lands.",
580
696
  ],
581
697
  }
582
698
  }
583
699
 
700
+ function duplicateDiagnostic(group: readonly DuplicateInstallFinding[]): Diagnostic | undefined {
701
+ const first = group[0]
702
+ if (first === undefined) return undefined
703
+ const context = `${first.topology === undefined ? "" : `. Topology: ${first.topology}`}${first.scope === undefined ? "" : `. Scope: ${first.scope}`}`
704
+ const skew = first.cause === "version-skew"
705
+ const names = group.map((finding) => finding.package)
706
+ return legacyDiagnostic("duplicate-install", {
707
+ severity: "error",
708
+ message:
709
+ group.length === 1
710
+ ? `${first.package} identity preflight found ${first.cause} - ${first.explanation}${context}`
711
+ : `${group.length} packages share one identity preflight finding, ${first.cause} - ${names.join(", ")} ${skew ? "each resolve to multiple versions and physical paths" : "are each loaded from more than one physical path"}, so module state and symbols are not shared${context}`,
712
+ fix:
713
+ group.length === 1 || skew
714
+ ? first.remediation
715
+ : `Deduplicate so each resolves to a single physical path, or declare them single-copy: add "nifra": { "singleCopy": [${names.map((name) => `"${name}"`).join(", ")}] } to package.json and preload "${SINGLE_COPY_REGISTER_SPECIFIER}" from bunfig.toml.`,
716
+ evidence: group.flatMap(copyLines),
717
+ suggestion: duplicateSuggestion(group),
718
+ })
719
+ }
720
+
584
721
  const doctorRules: readonly CheckRule[] = Object.freeze([
585
722
  {
586
723
  code: LEGACY_RULE_CODES["undeclared-dependency"]!,
@@ -610,20 +747,14 @@ const doctorRules: readonly CheckRule[] = Object.freeze([
610
747
  async scan(ctx: RuleContext) {
611
748
  const doctor = ctx.project.packages.doctor
612
749
  if (!doctor.ran) return []
613
- const findings: Diagnostic[] = []
750
+ // Duplicates with one cause and one layout (every package a linked sibling repo installs again)
751
+ // have one fix, so they are one finding; `identityPreflight` in --json keeps each package.
752
+ const groups = new Map<string, DuplicateInstallFinding[]>()
614
753
  for (const finding of doctor.duplicateInstalls) {
615
- const copies = copyLines(finding)
616
- findings.push(
617
- legacyDiagnostic("duplicate-install", {
618
- severity: "error",
619
- message: `${finding.package} identity preflight found ${finding.cause} - ${finding.explanation}${finding.topology === undefined ? "" : `. Topology: ${finding.topology}`}${finding.scope === undefined ? "" : `. Scope: ${finding.scope}`}`,
620
- fix: finding.remediation,
621
- evidence: copies,
622
- suggestion: duplicateSuggestion(finding),
623
- }),
624
- )
754
+ const key = [finding.cause, finding.topology ?? "", finding.scope ?? ""].join("\0")
755
+ groups.set(key, [...(groups.get(key) ?? []), finding])
625
756
  }
626
- return findings
757
+ return [...groups.values()].flatMap((group) => duplicateDiagnostic(group) ?? [])
627
758
  },
628
759
  },
629
760
  {
@@ -658,6 +789,26 @@ const doctorRules: readonly CheckRule[] = Object.freeze([
658
789
  },
659
790
  ])
660
791
 
792
+ /** Read from the doctor result check already collected; it spawns nothing and edits nothing. */
793
+ const versionDriftRule: CheckRule = {
794
+ code: "NF-D002",
795
+ title: "Mixed nifra versions",
796
+ async scan(ctx) {
797
+ const drift = ctx.project.packages.doctor.versionDrift
798
+ if (drift === undefined) return []
799
+ return [
800
+ diagnostic({
801
+ code: "NF-D002",
802
+ severity: "info",
803
+ message: versionDriftMessage(drift),
804
+ evidence: drift.versions.map(({ version }) => version),
805
+ fix: { recipe: "upgrade.align-versions", command: `nifra upgrade ${drift.highest}` },
806
+ verify: "nifra doctor",
807
+ }),
808
+ ]
809
+ },
810
+ }
811
+
661
812
  const pipelineRule: CheckRule = {
662
813
  code: LEGACY_RULE_CODES.pipeline!,
663
814
  title: TITLES.pipeline!,
@@ -951,7 +1102,9 @@ const contractRule: CheckRule = {
951
1102
  }),
952
1103
  ]
953
1104
  }
954
- if (contract.vacuous) {
1105
+ // `vacuous` describes the routes as they are now, so it only stands in for drift when the lock has
1106
+ // none: a lock whose schemas were all deleted is drift, not an empty contract.
1107
+ if (contract.vacuous && contract.error === undefined && contract.diagnostics.length === 0) {
955
1108
  return [
956
1109
  diagnostic({
957
1110
  code: LEGACY_RULE_CODES["contract-drift"]!,
@@ -995,6 +1148,7 @@ export const legacyRules: readonly CheckRule[] = Object.freeze([
995
1148
  interpolatedSqlRule,
996
1149
  responseRouteRule,
997
1150
  ...doctorRules,
1151
+ versionDriftRule,
998
1152
  pipelineRule,
999
1153
  serverManifestRule,
1000
1154
  manifestRule,