@nifrajs/cli 3.5.0 → 4.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 (403) hide show
  1. package/README.md +42 -3
  2. package/dist/app-files.d.ts +11 -0
  3. package/dist/app-files.d.ts.map +1 -0
  4. package/dist/app-files.js +34 -0
  5. package/dist/app-files.js.map +1 -0
  6. package/dist/assure-hydration.d.ts +7 -1
  7. package/dist/assure-hydration.d.ts.map +1 -1
  8. package/dist/assure-hydration.js +128 -43
  9. package/dist/assure-hydration.js.map +1 -1
  10. package/dist/assure.d.ts.map +1 -1
  11. package/dist/assure.js +16 -2
  12. package/dist/assure.js.map +1 -1
  13. package/dist/capabilities-tool.d.ts +5 -1
  14. package/dist/capabilities-tool.d.ts.map +1 -1
  15. package/dist/capabilities-tool.js +32 -10
  16. package/dist/capabilities-tool.js.map +1 -1
  17. package/dist/cdn-check.d.ts +41 -0
  18. package/dist/cdn-check.d.ts.map +1 -0
  19. package/dist/cdn-check.js +207 -0
  20. package/dist/cdn-check.js.map +1 -0
  21. package/dist/check-diagnostics.d.ts +4 -0
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +13 -0
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +49 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +291 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts.map +1 -1
  30. package/dist/check.js +99 -63
  31. package/dist/check.js.map +1 -1
  32. package/dist/cli.d.ts +42 -16
  33. package/dist/cli.d.ts.map +1 -1
  34. package/dist/cli.js +262 -82
  35. package/dist/cli.js.map +1 -1
  36. package/dist/command-catalog.d.ts +57 -5
  37. package/dist/command-catalog.d.ts.map +1 -1
  38. package/dist/command-catalog.js +270 -26
  39. package/dist/command-catalog.js.map +1 -1
  40. package/dist/config-target.d.ts +8 -0
  41. package/dist/config-target.d.ts.map +1 -0
  42. package/dist/config-target.js +45 -0
  43. package/dist/config-target.js.map +1 -0
  44. package/dist/contracts.d.ts.map +1 -1
  45. package/dist/contracts.js +52 -13
  46. package/dist/contracts.js.map +1 -1
  47. package/dist/db-audit.d.ts +37 -0
  48. package/dist/db-audit.d.ts.map +1 -0
  49. package/dist/db-audit.js +99 -0
  50. package/dist/db-audit.js.map +1 -0
  51. package/dist/db-child.d.ts +64 -0
  52. package/dist/db-child.d.ts.map +1 -0
  53. package/dist/db-child.js +213 -0
  54. package/dist/db-child.js.map +1 -0
  55. package/dist/db-config.d.ts +60 -0
  56. package/dist/db-config.d.ts.map +1 -0
  57. package/dist/db-config.js +176 -0
  58. package/dist/db-config.js.map +1 -0
  59. package/dist/db-tool.d.ts +84 -0
  60. package/dist/db-tool.d.ts.map +1 -0
  61. package/dist/db-tool.js +537 -0
  62. package/dist/db-tool.js.map +1 -0
  63. package/dist/dev-bun-config.d.ts +20 -13
  64. package/dist/dev-bun-config.d.ts.map +1 -1
  65. package/dist/dev-bun-config.js +98 -41
  66. package/dist/dev-bun-config.js.map +1 -1
  67. package/dist/dev-feed-tool.d.ts +106 -0
  68. package/dist/dev-feed-tool.d.ts.map +1 -0
  69. package/dist/dev-feed-tool.js +713 -0
  70. package/dist/dev-feed-tool.js.map +1 -0
  71. package/dist/dev-server-client.d.ts +67 -0
  72. package/dist/dev-server-client.d.ts.map +1 -0
  73. package/dist/dev-server-client.js +195 -0
  74. package/dist/dev-server-client.js.map +1 -0
  75. package/dist/diff-tool.d.ts +3 -3
  76. package/dist/diff-tool.d.ts.map +1 -1
  77. package/dist/diff-tool.js +6 -5
  78. package/dist/diff-tool.js.map +1 -1
  79. package/dist/docs-search.d.ts.map +1 -1
  80. package/dist/docs-search.js +12 -4
  81. package/dist/docs-search.js.map +1 -1
  82. package/dist/doctor.d.ts +17 -0
  83. package/dist/doctor.d.ts.map +1 -1
  84. package/dist/doctor.js +76 -15
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/env-file.d.ts +15 -0
  87. package/dist/env-file.d.ts.map +1 -1
  88. package/dist/env-file.js +73 -1
  89. package/dist/env-file.js.map +1 -1
  90. package/dist/fix-recipes.d.ts.map +1 -1
  91. package/dist/fix-recipes.js +41 -21
  92. package/dist/fix-recipes.js.map +1 -1
  93. package/dist/frontend-guidance.js +5 -5
  94. package/dist/frontend-guidance.js.map +1 -1
  95. package/dist/i18n-check.d.ts +16 -0
  96. package/dist/i18n-check.d.ts.map +1 -0
  97. package/dist/i18n-check.js +154 -0
  98. package/dist/i18n-check.js.map +1 -0
  99. package/dist/init-agents.d.ts +49 -13
  100. package/dist/init-agents.d.ts.map +1 -1
  101. package/dist/init-agents.js +239 -34
  102. package/dist/init-agents.js.map +1 -1
  103. package/dist/internal/code-unit-order.d.ts +4 -0
  104. package/dist/internal/code-unit-order.d.ts.map +1 -0
  105. package/dist/internal/code-unit-order.js +6 -0
  106. package/dist/internal/code-unit-order.js.map +1 -0
  107. package/dist/internal/script-kind.d.ts +7 -0
  108. package/dist/internal/script-kind.d.ts.map +1 -0
  109. package/dist/internal/script-kind.js +12 -0
  110. package/dist/internal/script-kind.js.map +1 -0
  111. package/dist/internal/source-facts.d.ts +5 -0
  112. package/dist/internal/source-facts.d.ts.map +1 -1
  113. package/dist/internal/source-facts.js +61 -5
  114. package/dist/internal/source-facts.js.map +1 -1
  115. package/dist/internal/typescript-import.d.ts +49 -24
  116. package/dist/internal/typescript-import.d.ts.map +1 -1
  117. package/dist/internal/typescript-import.js +507 -67
  118. package/dist/internal/typescript-import.js.map +1 -1
  119. package/dist/introspect.d.ts +5 -1
  120. package/dist/introspect.d.ts.map +1 -1
  121. package/dist/introspect.js +92 -27
  122. package/dist/introspect.js.map +1 -1
  123. package/dist/learn.js +3 -3
  124. package/dist/learn.js.map +1 -1
  125. package/dist/levels-tool.d.ts +10 -0
  126. package/dist/levels-tool.d.ts.map +1 -1
  127. package/dist/levels-tool.js +33 -3
  128. package/dist/levels-tool.js.map +1 -1
  129. package/dist/load.d.ts +67 -17
  130. package/dist/load.d.ts.map +1 -1
  131. package/dist/load.js +78 -18
  132. package/dist/load.js.map +1 -1
  133. package/dist/manifest-tool.d.ts.map +1 -1
  134. package/dist/manifest-tool.js +3 -2
  135. package/dist/manifest-tool.js.map +1 -1
  136. package/dist/mcp-app-tool.d.ts.map +1 -1
  137. package/dist/mcp-app-tool.js +2 -0
  138. package/dist/mcp-app-tool.js.map +1 -1
  139. package/dist/mcp-context.js +1 -1
  140. package/dist/mcp-context.js.map +1 -1
  141. package/dist/mcp-delegate.d.ts +57 -0
  142. package/dist/mcp-delegate.d.ts.map +1 -0
  143. package/dist/mcp-delegate.js +130 -0
  144. package/dist/mcp-delegate.js.map +1 -0
  145. package/dist/mcp-docs-tools.d.ts.map +1 -1
  146. package/dist/mcp-docs-tools.js +23 -5
  147. package/dist/mcp-docs-tools.js.map +1 -1
  148. package/dist/mcp-exec.d.ts +2 -0
  149. package/dist/mcp-exec.d.ts.map +1 -1
  150. package/dist/mcp-exec.js +132 -108
  151. package/dist/mcp-exec.js.map +1 -1
  152. package/dist/mcp-http.d.ts.map +1 -1
  153. package/dist/mcp-http.js +17 -5
  154. package/dist/mcp-http.js.map +1 -1
  155. package/dist/mcp-io.d.ts +14 -0
  156. package/dist/mcp-io.d.ts.map +1 -1
  157. package/dist/mcp-io.js +45 -0
  158. package/dist/mcp-io.js.map +1 -1
  159. package/dist/mcp-isolate.d.ts +48 -0
  160. package/dist/mcp-isolate.d.ts.map +1 -0
  161. package/dist/mcp-isolate.js +222 -0
  162. package/dist/mcp-isolate.js.map +1 -0
  163. package/dist/mcp-project-child.d.ts +74 -0
  164. package/dist/mcp-project-child.d.ts.map +1 -0
  165. package/dist/mcp-project-child.js +130 -0
  166. package/dist/mcp-project-child.js.map +1 -0
  167. package/dist/mcp-render.js +2 -2
  168. package/dist/mcp-render.js.map +1 -1
  169. package/dist/mcp-root.d.ts +37 -9
  170. package/dist/mcp-root.d.ts.map +1 -1
  171. package/dist/mcp-root.js +110 -23
  172. package/dist/mcp-root.js.map +1 -1
  173. package/dist/mcp-run.d.ts +18 -1
  174. package/dist/mcp-run.d.ts.map +1 -1
  175. package/dist/mcp-run.js +82 -9
  176. package/dist/mcp-run.js.map +1 -1
  177. package/dist/mcp-ws.js +4 -1
  178. package/dist/mcp-ws.js.map +1 -1
  179. package/dist/mcp.d.ts +16 -3
  180. package/dist/mcp.d.ts.map +1 -1
  181. package/dist/mcp.js +81 -53
  182. package/dist/mcp.js.map +1 -1
  183. package/dist/migrate-layout.d.ts +44 -0
  184. package/dist/migrate-layout.d.ts.map +1 -0
  185. package/dist/migrate-layout.js +982 -0
  186. package/dist/migrate-layout.js.map +1 -0
  187. package/dist/openapi-tool.d.ts +2 -2
  188. package/dist/openapi-tool.d.ts.map +1 -1
  189. package/dist/openapi-tool.js.map +1 -1
  190. package/dist/openapi-types.d.ts +2 -1
  191. package/dist/openapi-types.d.ts.map +1 -1
  192. package/dist/openapi-types.js +240 -12
  193. package/dist/openapi-types.js.map +1 -1
  194. package/dist/pipeline-report.d.ts +3 -2
  195. package/dist/pipeline-report.d.ts.map +1 -1
  196. package/dist/pipeline-report.js +19 -16
  197. package/dist/pipeline-report.js.map +1 -1
  198. package/dist/port.d.ts +5 -4
  199. package/dist/port.d.ts.map +1 -1
  200. package/dist/port.js +46 -29
  201. package/dist/port.js.map +1 -1
  202. package/dist/project-facts.d.ts +5 -0
  203. package/dist/project-facts.d.ts.map +1 -1
  204. package/dist/project-facts.js.map +1 -1
  205. package/dist/recipes/4.0.0.d.ts +9 -0
  206. package/dist/recipes/4.0.0.d.ts.map +1 -0
  207. package/dist/recipes/4.0.0.js +34 -0
  208. package/dist/recipes/4.0.0.js.map +1 -0
  209. package/dist/recipes/index.d.ts.map +1 -1
  210. package/dist/recipes/index.js +2 -0
  211. package/dist/recipes/index.js.map +1 -1
  212. package/dist/release-verification.d.ts +11 -1
  213. package/dist/release-verification.d.ts.map +1 -1
  214. package/dist/release-verification.js +47 -4
  215. package/dist/release-verification.js.map +1 -1
  216. package/dist/review.d.ts.map +1 -1
  217. package/dist/review.js +12 -8
  218. package/dist/review.js.map +1 -1
  219. package/dist/route-types.d.ts +28 -0
  220. package/dist/route-types.d.ts.map +1 -0
  221. package/dist/route-types.js +106 -0
  222. package/dist/route-types.js.map +1 -0
  223. package/dist/rules/codes.d.ts +9 -0
  224. package/dist/rules/codes.d.ts.map +1 -1
  225. package/dist/rules/codes.js +9 -0
  226. package/dist/rules/codes.js.map +1 -1
  227. package/dist/rules/data-guard.d.ts +12 -0
  228. package/dist/rules/data-guard.d.ts.map +1 -0
  229. package/dist/rules/data-guard.js +213 -0
  230. package/dist/rules/data-guard.js.map +1 -0
  231. package/dist/rules/index.d.ts +3 -0
  232. package/dist/rules/index.d.ts.map +1 -1
  233. package/dist/rules/index.js +17 -5
  234. package/dist/rules/index.js.map +1 -1
  235. package/dist/rules/islands.d.ts.map +1 -1
  236. package/dist/rules/islands.js +7 -4
  237. package/dist/rules/islands.js.map +1 -1
  238. package/dist/rules/legacy.d.ts.map +1 -1
  239. package/dist/rules/legacy.js +71 -38
  240. package/dist/rules/legacy.js.map +1 -1
  241. package/dist/rules/nano.d.ts.map +1 -1
  242. package/dist/rules/nano.js +15 -6
  243. package/dist/rules/nano.js.map +1 -1
  244. package/dist/rules/pages.d.ts +15 -0
  245. package/dist/rules/pages.d.ts.map +1 -0
  246. package/dist/rules/pages.js +88 -0
  247. package/dist/rules/pages.js.map +1 -0
  248. package/dist/rules/routes.d.ts +11 -0
  249. package/dist/rules/routes.d.ts.map +1 -1
  250. package/dist/rules/routes.js +180 -2
  251. package/dist/rules/routes.js.map +1 -1
  252. package/dist/rules/secrets.d.ts +10 -0
  253. package/dist/rules/secrets.d.ts.map +1 -0
  254. package/dist/rules/secrets.js +169 -0
  255. package/dist/rules/secrets.js.map +1 -0
  256. package/dist/rules/security.d.ts +7 -0
  257. package/dist/rules/security.d.ts.map +1 -1
  258. package/dist/rules/security.js +171 -65
  259. package/dist/rules/security.js.map +1 -1
  260. package/dist/rules/typescript.d.ts +16 -0
  261. package/dist/rules/typescript.d.ts.map +1 -0
  262. package/dist/rules/typescript.js +59 -0
  263. package/dist/rules/typescript.js.map +1 -0
  264. package/dist/rules/zones.d.ts +17 -0
  265. package/dist/rules/zones.d.ts.map +1 -0
  266. package/dist/rules/zones.js +174 -0
  267. package/dist/rules/zones.js.map +1 -0
  268. package/dist/scaffold.d.ts +25 -12
  269. package/dist/scaffold.d.ts.map +1 -1
  270. package/dist/scaffold.js +132 -87
  271. package/dist/scaffold.js.map +1 -1
  272. package/dist/sdk.d.ts +17 -3
  273. package/dist/sdk.d.ts.map +1 -1
  274. package/dist/sdk.js +941 -153
  275. package/dist/sdk.js.map +1 -1
  276. package/dist/search-terms.d.ts +2 -0
  277. package/dist/search-terms.d.ts.map +1 -1
  278. package/dist/search-terms.js +23 -8
  279. package/dist/search-terms.js.map +1 -1
  280. package/dist/smoke.d.ts +79 -0
  281. package/dist/smoke.d.ts.map +1 -0
  282. package/dist/smoke.js +494 -0
  283. package/dist/smoke.js.map +1 -0
  284. package/dist/sync-manifest.d.ts.map +1 -1
  285. package/dist/sync-manifest.js +2 -1
  286. package/dist/sync-manifest.js.map +1 -1
  287. package/dist/sync-routes.d.ts +4 -3
  288. package/dist/sync-routes.d.ts.map +1 -1
  289. package/dist/sync-routes.js +4 -3
  290. package/dist/sync-routes.js.map +1 -1
  291. package/dist/test-tool.d.ts.map +1 -1
  292. package/dist/test-tool.js +30 -11
  293. package/dist/test-tool.js.map +1 -1
  294. package/dist/types-search.d.ts.map +1 -1
  295. package/dist/types-search.js +11 -5
  296. package/dist/types-search.js.map +1 -1
  297. package/dist/upgrade.d.ts +36 -6
  298. package/dist/upgrade.d.ts.map +1 -1
  299. package/dist/upgrade.js +167 -29
  300. package/dist/upgrade.js.map +1 -1
  301. package/dist/verification-plan.d.ts.map +1 -1
  302. package/dist/verification-plan.js +4 -0
  303. package/dist/verification-plan.js.map +1 -1
  304. package/dist/verification.d.ts.map +1 -1
  305. package/dist/verification.js +45 -12
  306. package/dist/verification.js.map +1 -1
  307. package/dist/web-app-options.d.ts +19 -0
  308. package/dist/web-app-options.d.ts.map +1 -0
  309. package/dist/web-app-options.js +49 -0
  310. package/dist/web-app-options.js.map +1 -0
  311. package/dist/work-graph.d.ts.map +1 -1
  312. package/dist/work-graph.js +29 -13
  313. package/dist/work-graph.js.map +1 -1
  314. package/docs/examples.json +185 -29
  315. package/docs/llms-full.txt +3807 -580
  316. package/docs/types.json +14229 -10939
  317. package/package.json +19 -12
  318. package/src/app-files.ts +40 -0
  319. package/src/assure-hydration.ts +139 -46
  320. package/src/assure.ts +17 -2
  321. package/src/capabilities-tool.ts +35 -9
  322. package/src/cdn-check.ts +268 -0
  323. package/src/check-diagnostics.ts +17 -0
  324. package/src/check-scan.ts +358 -63
  325. package/src/check.ts +97 -62
  326. package/src/cli.ts +296 -87
  327. package/src/command-catalog.ts +374 -38
  328. package/src/config-target.ts +47 -0
  329. package/src/contracts.ts +54 -12
  330. package/src/db-audit.ts +128 -0
  331. package/src/db-child.ts +285 -0
  332. package/src/db-config.ts +224 -0
  333. package/src/db-tool.ts +690 -0
  334. package/src/dev-bun-config.ts +116 -40
  335. package/src/dev-feed-tool.ts +912 -0
  336. package/src/dev-server-client.ts +267 -0
  337. package/src/diff-tool.ts +6 -5
  338. package/src/docs-search.ts +10 -3
  339. package/src/doctor.ts +98 -15
  340. package/src/env-file.ts +78 -1
  341. package/src/fix-recipes.ts +44 -31
  342. package/src/frontend-guidance.ts +5 -5
  343. package/src/i18n-check.ts +179 -0
  344. package/src/init-agents.ts +301 -38
  345. package/src/internal/code-unit-order.ts +5 -0
  346. package/src/internal/script-kind.ts +11 -0
  347. package/src/internal/source-facts.ts +96 -22
  348. package/src/internal/typescript-import.ts +663 -68
  349. package/src/introspect.ts +90 -27
  350. package/src/learn.ts +3 -3
  351. package/src/levels-tool.ts +47 -2
  352. package/src/load.ts +140 -33
  353. package/src/manifest-tool.ts +3 -2
  354. package/src/mcp-app-tool.ts +2 -0
  355. package/src/mcp-context.ts +1 -1
  356. package/src/mcp-delegate.ts +162 -0
  357. package/src/mcp-docs-tools.ts +23 -5
  358. package/src/mcp-exec.ts +160 -132
  359. package/src/mcp-http.ts +19 -5
  360. package/src/mcp-io.ts +57 -0
  361. package/src/mcp-isolate.ts +287 -0
  362. package/src/mcp-project-child.ts +182 -0
  363. package/src/mcp-render.ts +2 -2
  364. package/src/mcp-root.ts +117 -22
  365. package/src/mcp-run.ts +104 -10
  366. package/src/mcp-ws.ts +4 -1
  367. package/src/mcp.ts +106 -64
  368. package/src/migrate-layout.ts +1121 -0
  369. package/src/openapi-tool.ts +7 -3
  370. package/src/openapi-types.ts +315 -13
  371. package/src/pipeline-report.ts +22 -18
  372. package/src/port.ts +53 -30
  373. package/src/project-facts.ts +5 -0
  374. package/src/recipes/4.0.0.ts +35 -0
  375. package/src/recipes/index.ts +2 -0
  376. package/src/release-verification.ts +74 -5
  377. package/src/review.ts +13 -8
  378. package/src/route-types.ts +133 -0
  379. package/src/rules/codes.ts +9 -0
  380. package/src/rules/data-guard.ts +230 -0
  381. package/src/rules/index.ts +18 -5
  382. package/src/rules/islands.ts +12 -4
  383. package/src/rules/legacy.ts +71 -39
  384. package/src/rules/nano.ts +18 -6
  385. package/src/rules/pages.ts +99 -0
  386. package/src/rules/routes.ts +181 -2
  387. package/src/rules/secrets.ts +174 -0
  388. package/src/rules/security.ts +198 -70
  389. package/src/rules/typescript.ts +77 -0
  390. package/src/rules/zones.ts +195 -0
  391. package/src/scaffold.ts +147 -95
  392. package/src/sdk.ts +1215 -158
  393. package/src/search-terms.ts +24 -7
  394. package/src/smoke.ts +607 -0
  395. package/src/sync-manifest.ts +2 -1
  396. package/src/sync-routes.ts +4 -3
  397. package/src/test-tool.ts +32 -11
  398. package/src/types-search.ts +11 -4
  399. package/src/upgrade.ts +205 -25
  400. package/src/verification-plan.ts +13 -0
  401. package/src/verification.ts +50 -12
  402. package/src/web-app-options.ts +57 -0
  403. package/src/work-graph.ts +27 -13
package/src/port.ts CHANGED
@@ -17,9 +17,11 @@
17
17
  import { join } from "node:path"
18
18
  import { Glob } from "bun"
19
19
  import { stripComments } from "./check.ts"
20
+ import { readConfigTarget } from "./config-target.ts"
21
+ import { codeUnitOrder } from "./internal/code-unit-order.ts"
20
22
 
21
23
  /** The five deploy targets a nifra `site` app can build for (mirrors create-nifra's DEPLOY presets). */
22
- export const TARGETS = ["bun", "node", "deno", "cf-pages", "vercel"] as const
24
+ export const TARGETS = ["bun", "node", "deno", "cloudflare", "vercel"] as const
23
25
  export type Target = (typeof TARGETS)[number]
24
26
 
25
27
  const isTarget = (value: string): value is Target => (TARGETS as readonly string[]).includes(value)
@@ -56,14 +58,14 @@ const inMemoryVerdicts: Readonly<Record<Target, Verdict>> = {
56
58
  bun: "caveat",
57
59
  node: "caveat",
58
60
  deno: "caveat",
59
- "cf-pages": "unsupported",
61
+ cloudflare: "unsupported",
60
62
  vercel: "unsupported",
61
63
  }
62
64
  const inMemoryReasons = (shared: string): Readonly<Record<Target, string>> => ({
63
65
  bun: "ok single-instance; state is lost on restart and not shared across instances",
64
66
  node: "ok single-instance; state is lost on restart and not shared across instances",
65
67
  deno: "ok single-instance; state is lost on restart and not shared across instances",
66
- "cf-pages": shared,
68
+ cloudflare: shared,
67
69
  vercel: shared,
68
70
  })
69
71
 
@@ -105,14 +107,14 @@ export const FEATURES: Readonly<Record<FeatureId, FeatureSpec>> = {
105
107
  bun: "ok",
106
108
  node: "ok",
107
109
  deno: "ok",
108
- "cf-pages": "caveat",
110
+ cloudflare: "caveat",
109
111
  vercel: "unsupported",
110
112
  },
111
113
  reasons: {
112
114
  bun: "long-lived process runs the scheduler loop",
113
115
  node: "long-lived process runs the scheduler loop",
114
116
  deno: "long-lived process runs the scheduler loop",
115
- "cf-pages":
117
+ cloudflare:
116
118
  "no long-lived loop on Workers - use the platform `scheduled` trigger via toFetchHandler(app, { scheduled }) and a wrangler [triggers] cron",
117
119
  vercel: "no long-lived process - use a Vercel Cron Job hitting an endpoint",
118
120
  },
@@ -124,14 +126,14 @@ export const FEATURES: Readonly<Record<FeatureId, FeatureSpec>> = {
124
126
  bun: "ok",
125
127
  node: "ok",
126
128
  deno: "ok",
127
- "cf-pages": "caveat",
129
+ cloudflare: "caveat",
128
130
  vercel: "unsupported",
129
131
  },
130
132
  reasons: {
131
133
  bun: "all sockets live in one process; app.publish broadcasts to every client",
132
134
  node: "all sockets live in one process; app.publish broadcasts to every client",
133
135
  deno: "all sockets live in one process; app.publish broadcasts to every client",
134
- "cf-pages":
136
+ cloudflare:
135
137
  "a stateless fetch can't broadcast across isolates - wrap the app in createWebSocketHub (a Durable Object) from @nifrajs/workers",
136
138
  vercel: "Vercel's serverless functions don't hold long-lived WebSocket connections",
137
139
  },
@@ -143,14 +145,14 @@ export const FEATURES: Readonly<Record<FeatureId, FeatureSpec>> = {
143
145
  bun: "ok",
144
146
  node: "unsupported",
145
147
  deno: "unsupported",
146
- "cf-pages": "unsupported",
148
+ cloudflare: "unsupported",
147
149
  vercel: "unsupported",
148
150
  },
149
151
  reasons: {
150
152
  bun: "the Bun runtime provides the Bun global",
151
153
  node: "no Bun global on Node - use the node: / Web equivalents",
152
154
  deno: "no Bun global on Deno - use the Deno / Web equivalents",
153
- "cf-pages": "no Bun global in the Workers runtime",
155
+ cloudflare: "no Bun global in the Workers runtime",
154
156
  vercel: "no Bun global in the Vercel runtime",
155
157
  },
156
158
  },
@@ -161,14 +163,14 @@ export const FEATURES: Readonly<Record<FeatureId, FeatureSpec>> = {
161
163
  bun: "unsupported",
162
164
  node: "unsupported",
163
165
  deno: "ok",
164
- "cf-pages": "unsupported",
166
+ cloudflare: "unsupported",
165
167
  vercel: "unsupported",
166
168
  },
167
169
  reasons: {
168
170
  bun: "no Deno global on Bun - use the Bun / Web equivalents",
169
171
  node: "no Deno global on Node - use the node: / Web equivalents",
170
172
  deno: "the Deno runtime provides the Deno global",
171
- "cf-pages": "no Deno global in the Workers runtime",
173
+ cloudflare: "no Deno global in the Workers runtime",
172
174
  vercel: "no Deno global in the Vercel runtime",
173
175
  },
174
176
  },
@@ -179,14 +181,14 @@ export const FEATURES: Readonly<Record<FeatureId, FeatureSpec>> = {
179
181
  bun: "ok",
180
182
  node: "ok",
181
183
  deno: "ok",
182
- "cf-pages": "caveat",
184
+ cloudflare: "caveat",
183
185
  vercel: "caveat",
184
186
  },
185
187
  reasons: {
186
188
  bun: "Bun implements the node: builtins",
187
189
  node: "native node: builtins",
188
190
  deno: "Deno implements the node: builtins",
189
- "cf-pages":
191
+ cloudflare:
190
192
  'Workers needs `compatibility_flags = ["nodejs_compat"]` in wrangler.toml (and not every builtin is covered)',
191
193
  vercel: "needs the Node.js runtime (not the Edge runtime) so node: builtins resolve",
192
194
  },
@@ -242,8 +244,9 @@ const WS_ROUTE = /\.ws\s*\(/g
242
244
  const WS_HUB = /(?<![.\w$])createWebSocketHub\b/
243
245
  // `Bun.` / `Deno.` runtime globals. `(?<![.\w$])` rejects `myBun.x`, `globalThis.Bun` stays a real use
244
246
  // (we want it), and `obj.Bun.` won't match because the char before `Bun` would be `.`.
245
- const BUN_GLOBAL = /(?<![.\w$])Bun\s*\./g
246
- const DENO_GLOBAL = /(?<![.\w$])Deno\s*\./g
247
+ const BUN_GLOBAL = /(?<![.\w$])(?:globalThis\.)?Bun\s*\./g
248
+ const DENO_GLOBAL = /(?<![.\w$])(?:globalThis\.)?Deno\s*\./g
249
+ const BUN_MODULE_IMPORT = /(?:from|import)\s*\(?\s*["'`]bun(?:\/[^"'`]*)?["'`]/g
247
250
  // `node:` builtin specifier in any import/require/export-from form.
248
251
  const NODE_BUILTIN = /(?:from|import|require)\s*\(?\s*["'`](node:[A-Za-z0-9/_-]+)["'`]/g
249
252
 
@@ -298,8 +301,12 @@ export function scanFileForFeatures(file: string, content: string, state: ScanSt
298
301
  // WS routes are collected unconditionally; the hub-presence post-pass decides whether they're a hazard.
299
302
  pushHits(add("in-process-websocket"), file, content, code, WS_ROUTE)
300
303
  pushHits(add("bun-runtime-api"), file, content, code, BUN_GLOBAL)
304
+ pushHits(add("bun-runtime-api"), file, content, code, BUN_MODULE_IMPORT)
301
305
  pushHits(add("deno-runtime-api"), file, content, code, DENO_GLOBAL)
302
306
  pushHits(add("node-builtin"), file, content, code, NODE_BUILTIN)
307
+ for (const evidence of state.evidence.values()) {
308
+ evidence.sort((a, b) => codeUnitOrder(a.file, b.file) || a.line - b.line)
309
+ }
303
310
  }
304
311
 
305
312
  // Mirror nifra check/doctor's ignore set: deps, build output, generated/per-runtime dist dirs, VCS.
@@ -323,7 +330,7 @@ async function walkPortSource(
323
330
  }
324
331
 
325
332
  const byEvidence = (a: Evidence, b: Evidence): number =>
326
- a.file.localeCompare(b.file) || a.line - b.line
333
+ codeUnitOrder(a.file, b.file) || a.line - b.line
327
334
 
328
335
  /** Run every detector over the project and return the detected features (sorted, stable). */
329
336
  export async function detectFeatures(cwd: string): Promise<DetectedFeature[]> {
@@ -353,12 +360,18 @@ export async function detectFeatures(cwd: string): Promise<DetectedFeature[]> {
353
360
  export interface ResolvedTarget {
354
361
  readonly target: Target
355
362
  /** How the target was determined - surfaced so the report explains itself. */
356
- readonly source: "flag" | "package-json-build" | "package-json-deploy" | "wrangler" | "vercel"
363
+ readonly source:
364
+ | "flag"
365
+ | "config"
366
+ | "package-json-build"
367
+ | "package-json-deploy"
368
+ | "wrangler"
369
+ | "vercel"
357
370
  }
358
371
 
359
372
  const BUILD_SCRIPT_TARGET: ReadonlyArray<readonly [RegExp, Target]> = [
360
373
  // create-nifra's canonical per-target build scripts (build-bun.ts / build-node.ts / build-deno.ts /
361
- // build-vercel.ts), plus the bare `build.ts` the cf-pages preset uses.
374
+ // build-vercel.ts), plus the bare `build.ts` the cloudflare preset uses.
362
375
  [/build-vercel\.[cm]?[jt]s\b/, "vercel"],
363
376
  [/build-deno\.[cm]?[jt]s\b/, "deno"],
364
377
  [/build-node\.[cm]?[jt]s\b/, "node"],
@@ -367,7 +380,7 @@ const BUILD_SCRIPT_TARGET: ReadonlyArray<readonly [RegExp, Target]> = [
367
380
  const DEPLOY_SCRIPT_TARGET: ReadonlyArray<readonly [RegExp, Target]> = [
368
381
  [/\bvercel\s+deploy\b/, "vercel"],
369
382
  [/\bdeployctl\b/, "deno"],
370
- [/wrangler\s+pages\s+deploy\b/, "cf-pages"],
383
+ [/wrangler\s+pages\s+deploy\b/, "cloudflare"],
371
384
  [/\bdocker\s+build\b/, "node"],
372
385
  ]
373
386
 
@@ -376,8 +389,9 @@ interface PackageJsonScripts {
376
389
  }
377
390
 
378
391
  /**
379
- * Resolve the app's deploy target. `--target` (validated) always wins. Otherwise infer it, in priority
380
- * order, from: the canonical `build` script (create-nifra's `build-<target>.ts`), the `deploy` script
392
+ * Resolve the app's deploy target. `--target` (validated) always wins, then the `target` that
393
+ * `nifra.config.ts` exports. Otherwise infer it, in priority order, from: the canonical `build` script
394
+ * (an older create-nifra's `build-<target>.ts`), the `deploy` script
381
395
  * (the vendor CLI it shells out to), a `wrangler.toml` (Cloudflare Pages), or a `vercel.json`. Returns
382
396
  * `undefined` when nothing signals a target (the matrix still prints; CI gating needs an explicit one).
383
397
  */
@@ -385,11 +399,18 @@ export async function resolveTarget(
385
399
  cwd: string,
386
400
  override?: string,
387
401
  ): Promise<ResolvedTarget | undefined> {
388
- if (override !== undefined) {
389
- if (!isTarget(override)) {
390
- throw new Error(`[nifra] invalid --target "${override}". options: ${TARGETS.join(", ")}`)
391
- }
392
- return { target: override, source: "flag" }
402
+ const checked = (value: string, label: string): Target => {
403
+ if (isTarget(value)) return value
404
+ throw new Error(
405
+ value === "cf-pages"
406
+ ? `[nifra] the ${label} "cf-pages" is now "cloudflare"`
407
+ : `[nifra] invalid ${label} "${value}". options: ${TARGETS.join(", ")}`,
408
+ )
409
+ }
410
+ if (override !== undefined) return { target: checked(override, "--target"), source: "flag" }
411
+ const configured = readConfigTarget(cwd)
412
+ if (configured !== undefined && configured !== "static") {
413
+ return { target: checked(configured, "target in nifra.config.ts"), source: "config" }
393
414
  }
394
415
 
395
416
  const pkg = await readJson<PackageJsonScripts>(join(cwd, "package.json"))
@@ -406,7 +427,7 @@ export async function resolveTarget(
406
427
  }
407
428
 
408
429
  if (await Bun.file(join(cwd, "wrangler.toml")).exists()) {
409
- return { target: "cf-pages", source: "wrangler" }
430
+ return { target: "cloudflare", source: "wrangler" }
410
431
  }
411
432
  if (
412
433
  (await Bun.file(join(cwd, "vercel.json")).exists()) ||
@@ -513,7 +534,7 @@ export async function collectPortResult(
513
534
  // --- Rendering ----------------------------------------------------------------------------------------
514
535
 
515
536
  const CELL: Readonly<Record<Verdict, string>> = { ok: "✓", caveat: "⚠", unsupported: "✗" }
516
- const COL_WIDTH = 9 // widest target header ("cf-pages") + padding
537
+ const COL_WIDTH = 12 // widest target header ("cloudflare") + padding
517
538
 
518
539
  const padCell = (s: string, width: number): string => {
519
540
  // Pad accounting for the symbol's display width (✓/⚠/✗ render as 1 cell in a monospace terminal).
@@ -530,7 +551,7 @@ export function renderReport(result: PortResult, opts: { readonly strict?: boole
530
551
  out.push(`target: ${resolved.target} (detected from ${describeSource(resolved.source)})`)
531
552
  } else {
532
553
  out.push(
533
- "target: not detected - pass --target <bun|node|deno|cf-pages|vercel> to gate against one",
554
+ "target: not detected - pass --target <bun|node|deno|cloudflare|vercel> to gate against one",
534
555
  )
535
556
  }
536
557
  out.push("")
@@ -610,6 +631,8 @@ function describeSource(source: ResolvedTarget["source"]): string {
610
631
  switch (source) {
611
632
  case "flag":
612
633
  return "--target"
634
+ case "config":
635
+ return "nifra.config.ts"
613
636
  case "package-json-build":
614
637
  return "the package.json build script"
615
638
  case "package-json-deploy":
@@ -655,7 +678,7 @@ export async function runPort(cwd: string, opts: RunPortOptions = {}): Promise<b
655
678
  if (result.resolved === undefined) {
656
679
  if (!opts.json) {
657
680
  console.error(
658
- "\n[nifra] --ci needs a deploy target to gate against, and none was detected. Pass --target <bun|node|deno|cf-pages|vercel>.",
681
+ "\n[nifra] --ci needs a deploy target to gate against, and none was detected. Pass --target <bun|node|deno|cloudflare|vercel>.",
659
682
  )
660
683
  }
661
684
  return false
@@ -11,6 +11,7 @@ import type {
11
11
  TransitiveServerImportFinding,
12
12
  } from "./check-scan.ts"
13
13
  import type { DoctorResult } from "./doctor.ts"
14
+ import type { TypeScriptSession } from "./internal/typescript-import.ts"
14
15
  import type { PipelineReport } from "./pipeline-report.ts"
15
16
  import type { RulePack, SourceIndex } from "./rules/index.ts"
16
17
 
@@ -47,6 +48,10 @@ export interface ContractCheckFacts {
47
48
  export interface ProjectCheckFacts {
48
49
  readonly typecheck: CheckTypecheckResult
49
50
  readonly sqlCompilerAvailable: boolean
51
+ /** Set when a project TypeScript install is present but outside the scanner adapter range. */
52
+ readonly unsupportedTypeScriptVersion?: string
53
+ /** Shared syntax/checker session for built-in rules; omitted from serialized check output. */
54
+ readonly typescriptSession?: TypeScriptSession
50
55
  readonly checkConfigError?: string
51
56
  readonly checkConfigWarnings: readonly string[]
52
57
  readonly contracts: ContractCheckFacts
@@ -0,0 +1,35 @@
1
+ import type { UpgradeRecipe } from "./index.ts"
2
+
3
+ /**
4
+ * Upgrade recipe for Nifra 4.0.
5
+ *
6
+ * The runner pins the fixed group and moves the retired `server-only` marker import. The layout move
7
+ * is `nifra migrate layout`'s job, so the notes lead with it, then cover what an app may observe.
8
+ */
9
+ export const recipe: UpgradeRecipe = {
10
+ version: "4.0.0",
11
+ pins: [
12
+ { match: "@nifrajs/", to: "4.0.0" },
13
+ { match: "create-nifra", to: "4.0.0" },
14
+ { match: "nifra", to: "4.0.0" },
15
+ ],
16
+ importMoves: [{ from: "@nifrajs/web/server-only", to: "@nifrajs/web/backend-only" }],
17
+ notes: [
18
+ "Run `nifra migrate layout` (a dry run; `--write` applies it). An app's code now lives in `routes/`, `frontend/`, `backend/` and `shared/`: each route splits into `x.tsx` (the page) and `x.backend.ts` (`loader`, `action`, `middleware` and the other server exports), `_middleware.ts` folds into `_layout.backend.ts`, the root `backend.ts` and `framework.ts` move to `backend/app.ts` and `backend/framework.ts`, `*.server` modules move under `backend/`, and `ServerOnly` becomes `BackendOnly`. It lists what it could not decide.",
19
+ "Browser builds and dev servers refuse backend code, server packages and any first-party file in no zone, naming the import chain; `nifra check` reports the same (NF-C004, NF-C028) and private environment reads in browser code (NF-C029).",
20
+ "A loader or action that returns data needs `loaderOutput` / `actionOutput` in its backend half, or the request fails; the data is projected through the schema, so undeclared keys never reach the page. A deferred value is declared with `t.deferred(schema)`. `nifra check` reports a loader without one (NF-C030).",
21
+ "Builds fail on what looks like a credential in a client bundle, a `public/` file or a prerendered page; exempt a reviewed false positive with `secretExemptions` in `nifra.config.ts`.",
22
+ "The Cloudflare Pages target is `cloudflare` (`cf-pages` is refused), and `export const target` in `nifra.config.ts` sets what `nifra build` emits.",
23
+ '`withISR` no longer caches a page per query string by default: a request with any query parameter renders fresh and is not stored. Pass `query: ["page", ...]` to cache those parameters, or `query: "all"` to both `withISR` and `revalidateEndpoint` to keep the old keys.',
24
+ "A page file under the backend's API prefix or any other mount (`routes/api/*.tsx` with a backend) now fails at startup, at build and in `nifra check` (NF-C027), instead of answering 404. Move the page, change `apiPrefix` in `backend/framework.ts`, or serve that path from the backend.",
25
+ "Request methods match exactly: a request sent as `post` or `Patch` answers 405 or 404 instead of reaching the `POST`/`PATCH` route.",
26
+ "A request path's `.` and `..` segments (also `%2e` and backslashes) are resolved before routing on every runtime, so `/users/../posts` routes to `/posts` and `c.req.url` shows the resolved path.",
27
+ "A JSON body is parsed only when its media type is `application/json` or `application/*+json`; `text/plain; x=application/json` no longer parses, and server functions answer it with 415.",
28
+ "`revalidateEndpoint()` and `previewEndpoint()` throw at construction without a `secret`.",
29
+ 'Rendering a hydrating page with an empty `clientEntry` throws instead of emitting `<script src="">`.',
30
+ 'The typed client refuses a `.` or `..` path param value (`{ ok: false, status: 0, error: { error: "invalid_path" } }`) instead of sending a request to another path.',
31
+ "`jwt()`, `verifyCsrfToken()` and `verifyDownloadUrl()` accept only canonical unpadded base64url signatures.",
32
+ "In plural messages, `#` is formatted in the locale's number format (`1,000 items`), not as plain digits.",
33
+ "`solidBunPlugin` is exported from `@nifrajs/web-solid/plugin` and `svelteBunPlugin` from `@nifrajs/web-svelte/plugin`; the adapter roots export only the render adapter. Run `nifra fix --code NF-C005` to rewrite the imports.",
34
+ ],
35
+ }
@@ -7,6 +7,7 @@
7
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
+ import { recipe as v4_0_0 } from "./4.0.0.ts"
10
11
 
11
12
  /** A pin rule: every dependency whose name starts with `match` is set to version `to`. */
12
13
  export interface PinRule {
@@ -42,6 +43,7 @@ const RECIPES: Record<string, UpgradeRecipe> = {
42
43
  [v1_8_0.version]: v1_8_0,
43
44
  [v2_0_0.version]: v2_0_0,
44
45
  [v3_0_0.version]: v3_0_0,
46
+ [v4_0_0.version]: v4_0_0,
45
47
  }
46
48
 
47
49
  export function getRecipe(version: string): UpgradeRecipe | undefined {
@@ -16,7 +16,8 @@ import {
16
16
  } from "./verification-plan.ts"
17
17
 
18
18
  export type ReleaseVerificationMode = VerificationPlanMode
19
- export type ReleaseGateStatus = "pass" | "fail" | "skipped"
19
+ /** `undeclared`: the project's package.json has no script for the gate, so it was not run. */
20
+ export type ReleaseGateStatus = "pass" | "fail" | "skipped" | "undeclared"
20
21
 
21
22
  export interface ReleaseGateResult {
22
23
  readonly id: string
@@ -24,6 +25,8 @@ export interface ReleaseGateResult {
24
25
  readonly commands: readonly string[]
25
26
  readonly exitCode?: number
26
27
  readonly message?: string
28
+ /** The last lines a failed gate's command printed, so a CI log shows why it failed. */
29
+ readonly output?: string
27
30
  readonly remediation: string
28
31
  }
29
32
 
@@ -33,6 +36,8 @@ export interface ReleaseVerificationResult {
33
36
  readonly gates: readonly ReleaseGateResult[]
34
37
  /** Release gates intentionally absent from the selected shorter plan. */
35
38
  readonly omittedReleaseGateIds: readonly string[]
39
+ /** Gates a shared runner left out; a local verification runs them. */
40
+ readonly localGateIds: readonly string[]
36
41
  }
37
42
 
38
43
  export interface ReleaseCommandResult {
@@ -49,6 +54,11 @@ export interface ReleaseCommandSpec {
49
54
 
50
55
  export interface ReleaseVerificationOptions {
51
56
  readonly mode?: ReleaseVerificationMode
57
+ /**
58
+ * Leave out the gates the plan keeps off CI (`workflowRequired: false`): timing on a shared
59
+ * runner swings too far run to run to gate a release on.
60
+ */
61
+ readonly sharedRunner?: boolean
52
62
  readonly runCommand?: (spec: ReleaseCommandSpec) => Promise<ReleaseCommandResult>
53
63
  }
54
64
 
@@ -67,6 +77,18 @@ const runBunCommand = async (spec: ReleaseCommandSpec): Promise<ReleaseCommandRe
67
77
  return { exitCode, stdout, stderr }
68
78
  }
69
79
 
80
+ const OUTPUT_TAIL_LINES = 40
81
+
82
+ /** The last lines a command printed, stdout before stderr, or undefined when it printed nothing. */
83
+ const outputTail = (result: ReleaseCommandResult): string | undefined => {
84
+ const printed = [result.stdout, result.stderr]
85
+ .map((text) => text?.trimEnd() ?? "")
86
+ .filter((text) => text !== "")
87
+ return printed.length === 0
88
+ ? undefined
89
+ : printed.join("\n").split("\n").slice(-OUTPUT_TAIL_LINES).join("\n")
90
+ }
91
+
70
92
  const parsePackage = (path: string): Record<string, unknown> | undefined => {
71
93
  try {
72
94
  const value: unknown = JSON.parse(readFileSync(path, "utf8"))
@@ -153,7 +175,7 @@ const runGate = async (
153
175
  })
154
176
  lastExitCode = result.exitCode
155
177
  if (result.exitCode !== 0) {
156
- return {
178
+ const failed: Omit<ReleaseGateResult, "output"> & { output?: string } = {
157
179
  id: gate.id,
158
180
  status: "fail",
159
181
  commands: gate.commands.map(commandLabel),
@@ -161,6 +183,9 @@ const runGate = async (
161
183
  message: `${commandLabel(args)} exited with code ${result.exitCode}`,
162
184
  remediation: gate.remediation,
163
185
  }
186
+ const output = outputTail(result)
187
+ if (output !== undefined) failed.output = output
188
+ return failed
164
189
  }
165
190
  }
166
191
  return {
@@ -180,6 +205,23 @@ const skippedGate = (gate: VerificationGateSpec, failedId: string): ReleaseGateR
180
205
  remediation: `Fix the ${failedId} gate first, then rerun verification.`,
181
206
  })
182
207
 
208
+ const undeclaredGate = (gate: VerificationGateSpec, script: string): ReleaseGateResult => ({
209
+ id: gate.id,
210
+ status: "undeclared",
211
+ commands: gate.commands.map(commandLabel),
212
+ message: `\`${script}\` is not a script in package.json`,
213
+ remediation: `Declare a \`${script}\` script to run this gate.`,
214
+ })
215
+
216
+ /** The first `bun run <script>` of a gate that the root package.json does not declare. */
217
+ const undeclaredScript = (
218
+ gate: VerificationGateSpec,
219
+ scripts: ReadonlySet<string>,
220
+ ): string | undefined =>
221
+ gate.commands.find(
222
+ ([verb, script]) => verb === "run" && script !== undefined && !scripts.has(script),
223
+ )?.[1]
224
+
183
225
  /** Collect the same gate result rendered by the CLI and used by repository scripts. */
184
226
  export async function collectReleaseVerification(
185
227
  start: string,
@@ -189,14 +231,27 @@ export async function collectReleaseVerification(
189
231
  const root = await resolveVerificationRoot(start)
190
232
  const runCommand = options.runCommand ?? runBunCommand
191
233
  const fixtureRoot = mkdtempSync(join(realpathSync(tmpdir()), "nifra-verify-"))
234
+ const declared = parsePackage(join(root, "package.json"))?.scripts
235
+ const scripts = new Set(
236
+ declared !== null && typeof declared === "object" ? Object.keys(declared) : [],
237
+ )
238
+ const plan = gatePlan(mode)
239
+ const local = options.sharedRunner === true ? plan.filter((gate) => !gate.workflowRequired) : []
192
240
  const gates: ReleaseGateResult[] = []
193
241
  try {
194
242
  let failedId: string | undefined
195
- for (const gate of gatePlan(mode)) {
243
+ for (const gate of plan) {
244
+ if (local.includes(gate)) continue
196
245
  if (failedId !== undefined) {
197
246
  gates.push(skippedGate(gate, failedId))
198
247
  continue
199
248
  }
249
+ // The plan names the nifra repository's own scripts; a project without one has nothing to run.
250
+ const missing = undeclaredScript(gate, scripts)
251
+ if (missing !== undefined) {
252
+ gates.push(undeclaredGate(gate, missing))
253
+ continue
254
+ }
200
255
  const result = await runGate(root, gate, runCommand, fixtureRoot)
201
256
  gates.push(result)
202
257
  if (result.status === "fail") failedId = result.id
@@ -205,10 +260,13 @@ export async function collectReleaseVerification(
205
260
  rmSync(fixtureRoot, { recursive: true, force: true })
206
261
  }
207
262
  return {
208
- ok: gates.every((gate) => gate.status === "pass"),
263
+ ok:
264
+ gates.some((gate) => gate.status === "pass") &&
265
+ gates.every((gate) => gate.status === "pass" || gate.status === "undeclared"),
209
266
  mode,
210
267
  gates,
211
268
  omittedReleaseGateIds: omittedVerificationGateIds(mode),
269
+ localGateIds: local.map((gate) => gate.id),
212
270
  }
213
271
  }
214
272
 
@@ -218,7 +276,10 @@ export function renderReleaseVerification(result: ReleaseVerificationResult): st
218
276
  const marker = gate.status === "pass" ? "✓" : gate.status === "fail" ? "✗" : "-"
219
277
  lines.push(`${marker} ${gate.id}: ${gate.status}`)
220
278
  if (gate.message !== undefined) lines.push(` ${gate.message}`)
221
- if (gate.status !== "pass") lines.push(` fix: ${gate.remediation}`)
279
+ if (gate.output !== undefined)
280
+ lines.push(...gate.output.split("\n").map((line) => ` | ${line}`))
281
+ if (gate.status === "fail" || gate.status === "skipped")
282
+ lines.push(` fix: ${gate.remediation}`)
222
283
  }
223
284
  if (result.omittedReleaseGateIds.length > 0) {
224
285
  lines.push(
@@ -227,6 +288,13 @@ export function renderReleaseVerification(result: ReleaseVerificationResult): st
227
288
  " run `bun run check:release` for the full release gate.",
228
289
  )
229
290
  }
291
+ if (result.localGateIds.length > 0) {
292
+ lines.push(
293
+ "",
294
+ `note: a shared runner leaves out the timing gates: ${result.localGateIds.join(", ")}`,
295
+ " run `bun run check:release` locally for them.",
296
+ )
297
+ }
230
298
  lines.push("", result.ok ? "✓ verification passed" : "✗ verification failed")
231
299
  return lines.join("\n")
232
300
  }
@@ -246,6 +314,7 @@ if (import.meta.main) {
246
314
  const mode: ReleaseVerificationMode = process.argv.includes("--release") ? "release" : "default"
247
315
  const ok = await runReleaseVerification(process.cwd(), {
248
316
  mode,
317
+ sharedRunner: process.argv.includes("--shared-runner"),
249
318
  json: process.argv.includes("--json"),
250
319
  })
251
320
  if (!ok) process.exitCode = 1
package/src/review.ts CHANGED
@@ -615,23 +615,20 @@ async function composeCollectedReport(
615
615
  const changedPaths = new Set(scope.changedPaths)
616
616
  const retainedByCheck = new Map<ReviewCheckId, CandidateFinding[]>()
617
617
  let totalOutOfScope = 0
618
+ let typeErrorOutOfScope = false
618
619
  for (const id of CHECK_IDS) {
619
- const source = candidatesByCheck.get(id) ?? []
620
620
  const retained: CandidateFinding[] = []
621
- let outOfScope = 0
622
- for (const candidate of source) {
621
+ for (const candidate of candidatesByCheck.get(id) ?? []) {
623
622
  if (
624
623
  scope.kind === "diff" &&
625
624
  candidate.location !== undefined &&
626
625
  !changedPaths.has(candidate.location.path)
627
626
  ) {
628
- outOfScope += 1
629
627
  totalOutOfScope += 1
628
+ if (id === "typecheck" && candidate.severity === "error") typeErrorOutOfScope = true
630
629
  } else retained.push(candidate)
631
630
  }
632
631
  retainedByCheck.set(id, retained)
633
- // Store the per-check count temporarily on the scope map below through a side channel-free map.
634
- void outOfScope
635
632
  }
636
633
 
637
634
  const checks: ReviewCheckResult[] = []
@@ -653,6 +650,11 @@ async function composeCollectedReport(
653
650
  : checkResult.typecheckNote?.toLowerCase().includes("typescript")
654
651
  ? "missing-typescript"
655
652
  : "unsupported"
653
+ } else if (id === "typecheck" && typeErrorOutOfScope) {
654
+ // A change can break a caller it never touched, so a type error outside the diff cannot be
655
+ // ruled out as this change's: the required check stays open instead of passing.
656
+ status = "unavailable"
657
+ reasonCode = "filtered-out-of-scope"
656
658
  } else if (!configured) {
657
659
  status = "skipped"
658
660
  reasonCode = "not-configured"
@@ -671,7 +673,8 @@ async function composeCollectedReport(
671
673
  id,
672
674
  required: REQUIRED_CHECKS.has(id),
673
675
  status,
674
- duration: status === "unavailable" ? "timeout" : "standard",
676
+ duration:
677
+ status === "unavailable" && reasonCode !== "filtered-out-of-scope" ? "timeout" : "standard",
675
678
  counts: countsFor(retained, outOfScope),
676
679
  findingIds: retained.map((candidate) => candidate.finding.id),
677
680
  evidence: checkEvidence,
@@ -942,7 +945,9 @@ export function renderReviewReport(report: ReviewReport): readonly string[] {
942
945
  for (const check of report.checks) {
943
946
  if (check.status === "skipped") lines.push(` skipped ${check.id}`)
944
947
  if (check.status === "unavailable" || check.status === "error")
945
- lines.push(` unavailable ${check.id}`)
948
+ lines.push(
949
+ ` unavailable ${check.id}${check.reasonCode === undefined ? "" : ` (${check.reasonCode})`}`,
950
+ )
946
951
  }
947
952
  return lines
948
953
  }