@nifrajs/cli 3.3.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 (425) 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 +7 -15
  22. package/dist/check-diagnostics.d.ts.map +1 -1
  23. package/dist/check-diagnostics.js +162 -713
  24. package/dist/check-diagnostics.js.map +1 -1
  25. package/dist/check-scan.d.ts +58 -7
  26. package/dist/check-scan.d.ts.map +1 -1
  27. package/dist/check-scan.js +323 -62
  28. package/dist/check-scan.js.map +1 -1
  29. package/dist/check.d.ts +1 -0
  30. package/dist/check.d.ts.map +1 -1
  31. package/dist/check.js +145 -54
  32. package/dist/check.js.map +1 -1
  33. package/dist/cli.d.ts +42 -16
  34. package/dist/cli.d.ts.map +1 -1
  35. package/dist/cli.js +306 -87
  36. package/dist/cli.js.map +1 -1
  37. package/dist/command-catalog.d.ts +60 -5
  38. package/dist/command-catalog.d.ts.map +1 -1
  39. package/dist/command-catalog.js +298 -33
  40. package/dist/command-catalog.js.map +1 -1
  41. package/dist/config-target.d.ts +8 -0
  42. package/dist/config-target.d.ts.map +1 -0
  43. package/dist/config-target.js +45 -0
  44. package/dist/config-target.js.map +1 -0
  45. package/dist/contracts.d.ts +3 -2
  46. package/dist/contracts.d.ts.map +1 -1
  47. package/dist/contracts.js +56 -16
  48. package/dist/contracts.js.map +1 -1
  49. package/dist/db-audit.d.ts +37 -0
  50. package/dist/db-audit.d.ts.map +1 -0
  51. package/dist/db-audit.js +99 -0
  52. package/dist/db-audit.js.map +1 -0
  53. package/dist/db-child.d.ts +64 -0
  54. package/dist/db-child.d.ts.map +1 -0
  55. package/dist/db-child.js +213 -0
  56. package/dist/db-child.js.map +1 -0
  57. package/dist/db-config.d.ts +60 -0
  58. package/dist/db-config.d.ts.map +1 -0
  59. package/dist/db-config.js +176 -0
  60. package/dist/db-config.js.map +1 -0
  61. package/dist/db-tool.d.ts +84 -0
  62. package/dist/db-tool.d.ts.map +1 -0
  63. package/dist/db-tool.js +537 -0
  64. package/dist/db-tool.js.map +1 -0
  65. package/dist/dev-bun-config.d.ts +20 -13
  66. package/dist/dev-bun-config.d.ts.map +1 -1
  67. package/dist/dev-bun-config.js +98 -41
  68. package/dist/dev-bun-config.js.map +1 -1
  69. package/dist/dev-feed-tool.d.ts +106 -0
  70. package/dist/dev-feed-tool.d.ts.map +1 -0
  71. package/dist/dev-feed-tool.js +713 -0
  72. package/dist/dev-feed-tool.js.map +1 -0
  73. package/dist/dev-server-client.d.ts +67 -0
  74. package/dist/dev-server-client.d.ts.map +1 -0
  75. package/dist/dev-server-client.js +195 -0
  76. package/dist/dev-server-client.js.map +1 -0
  77. package/dist/diagnostics.d.ts +86 -0
  78. package/dist/diagnostics.d.ts.map +1 -1
  79. package/dist/diagnostics.js +85 -0
  80. package/dist/diagnostics.js.map +1 -1
  81. package/dist/diff-tool.d.ts +3 -3
  82. package/dist/diff-tool.d.ts.map +1 -1
  83. package/dist/diff-tool.js +6 -5
  84. package/dist/diff-tool.js.map +1 -1
  85. package/dist/docs-search.d.ts.map +1 -1
  86. package/dist/docs-search.js +12 -4
  87. package/dist/docs-search.js.map +1 -1
  88. package/dist/doctor.d.ts +17 -0
  89. package/dist/doctor.d.ts.map +1 -1
  90. package/dist/doctor.js +76 -15
  91. package/dist/doctor.js.map +1 -1
  92. package/dist/env-file.d.ts +15 -0
  93. package/dist/env-file.d.ts.map +1 -1
  94. package/dist/env-file.js +73 -1
  95. package/dist/env-file.js.map +1 -1
  96. package/dist/fix-recipes.d.ts.map +1 -1
  97. package/dist/fix-recipes.js +41 -21
  98. package/dist/fix-recipes.js.map +1 -1
  99. package/dist/frontend-guidance.js +5 -5
  100. package/dist/frontend-guidance.js.map +1 -1
  101. package/dist/i18n-check.d.ts +16 -0
  102. package/dist/i18n-check.d.ts.map +1 -0
  103. package/dist/i18n-check.js +154 -0
  104. package/dist/i18n-check.js.map +1 -0
  105. package/dist/init-agents.d.ts +49 -13
  106. package/dist/init-agents.d.ts.map +1 -1
  107. package/dist/init-agents.js +239 -34
  108. package/dist/init-agents.js.map +1 -1
  109. package/dist/internal/code-unit-order.d.ts +4 -0
  110. package/dist/internal/code-unit-order.d.ts.map +1 -0
  111. package/dist/internal/code-unit-order.js +6 -0
  112. package/dist/internal/code-unit-order.js.map +1 -0
  113. package/dist/internal/script-kind.d.ts +7 -0
  114. package/dist/internal/script-kind.d.ts.map +1 -0
  115. package/dist/internal/script-kind.js +12 -0
  116. package/dist/internal/script-kind.js.map +1 -0
  117. package/dist/internal/source-facts.d.ts +5 -0
  118. package/dist/internal/source-facts.d.ts.map +1 -1
  119. package/dist/internal/source-facts.js +61 -5
  120. package/dist/internal/source-facts.js.map +1 -1
  121. package/dist/internal/typescript-import.d.ts +49 -24
  122. package/dist/internal/typescript-import.d.ts.map +1 -1
  123. package/dist/internal/typescript-import.js +507 -67
  124. package/dist/internal/typescript-import.js.map +1 -1
  125. package/dist/introspect.d.ts +6 -4
  126. package/dist/introspect.d.ts.map +1 -1
  127. package/dist/introspect.js +93 -74
  128. package/dist/introspect.js.map +1 -1
  129. package/dist/learn.js +3 -3
  130. package/dist/learn.js.map +1 -1
  131. package/dist/levels-tool.d.ts +10 -0
  132. package/dist/levels-tool.d.ts.map +1 -1
  133. package/dist/levels-tool.js +33 -3
  134. package/dist/levels-tool.js.map +1 -1
  135. package/dist/load.d.ts +71 -16
  136. package/dist/load.d.ts.map +1 -1
  137. package/dist/load.js +78 -18
  138. package/dist/load.js.map +1 -1
  139. package/dist/manifest-tool.d.ts.map +1 -1
  140. package/dist/manifest-tool.js +3 -2
  141. package/dist/manifest-tool.js.map +1 -1
  142. package/dist/mcp-app-tool.d.ts.map +1 -1
  143. package/dist/mcp-app-tool.js +2 -0
  144. package/dist/mcp-app-tool.js.map +1 -1
  145. package/dist/mcp-context.d.ts.map +1 -1
  146. package/dist/mcp-context.js +5 -6
  147. package/dist/mcp-context.js.map +1 -1
  148. package/dist/mcp-delegate.d.ts +57 -0
  149. package/dist/mcp-delegate.d.ts.map +1 -0
  150. package/dist/mcp-delegate.js +130 -0
  151. package/dist/mcp-delegate.js.map +1 -0
  152. package/dist/mcp-docs-tools.d.ts.map +1 -1
  153. package/dist/mcp-docs-tools.js +23 -5
  154. package/dist/mcp-docs-tools.js.map +1 -1
  155. package/dist/mcp-exec.d.ts +3 -0
  156. package/dist/mcp-exec.d.ts.map +1 -1
  157. package/dist/mcp-exec.js +263 -131
  158. package/dist/mcp-exec.js.map +1 -1
  159. package/dist/mcp-http.d.ts.map +1 -1
  160. package/dist/mcp-http.js +20 -5
  161. package/dist/mcp-http.js.map +1 -1
  162. package/dist/mcp-io.d.ts +26 -0
  163. package/dist/mcp-io.d.ts.map +1 -1
  164. package/dist/mcp-io.js +113 -0
  165. package/dist/mcp-io.js.map +1 -1
  166. package/dist/mcp-isolate.d.ts +48 -0
  167. package/dist/mcp-isolate.d.ts.map +1 -0
  168. package/dist/mcp-isolate.js +222 -0
  169. package/dist/mcp-isolate.js.map +1 -0
  170. package/dist/mcp-path.d.ts +10 -0
  171. package/dist/mcp-path.d.ts.map +1 -0
  172. package/dist/mcp-path.js +51 -0
  173. package/dist/mcp-path.js.map +1 -0
  174. package/dist/mcp-project-child.d.ts +74 -0
  175. package/dist/mcp-project-child.d.ts.map +1 -0
  176. package/dist/mcp-project-child.js +130 -0
  177. package/dist/mcp-project-child.js.map +1 -0
  178. package/dist/mcp-reflect.d.ts.map +1 -1
  179. package/dist/mcp-reflect.js +2 -2
  180. package/dist/mcp-reflect.js.map +1 -1
  181. package/dist/mcp-render.d.ts.map +1 -1
  182. package/dist/mcp-render.js +64 -45
  183. package/dist/mcp-render.js.map +1 -1
  184. package/dist/mcp-root.d.ts +37 -9
  185. package/dist/mcp-root.d.ts.map +1 -1
  186. package/dist/mcp-root.js +124 -25
  187. package/dist/mcp-root.js.map +1 -1
  188. package/dist/mcp-run.d.ts +18 -1
  189. package/dist/mcp-run.d.ts.map +1 -1
  190. package/dist/mcp-run.js +159 -55
  191. package/dist/mcp-run.js.map +1 -1
  192. package/dist/mcp-ws.js +4 -1
  193. package/dist/mcp-ws.js.map +1 -1
  194. package/dist/mcp.d.ts +18 -5
  195. package/dist/mcp.d.ts.map +1 -1
  196. package/dist/mcp.js +165 -82
  197. package/dist/mcp.js.map +1 -1
  198. package/dist/migrate-layout.d.ts +44 -0
  199. package/dist/migrate-layout.d.ts.map +1 -0
  200. package/dist/migrate-layout.js +982 -0
  201. package/dist/migrate-layout.js.map +1 -0
  202. package/dist/openapi-tool.d.ts +2 -2
  203. package/dist/openapi-tool.d.ts.map +1 -1
  204. package/dist/openapi-tool.js.map +1 -1
  205. package/dist/openapi-types.d.ts +2 -1
  206. package/dist/openapi-types.d.ts.map +1 -1
  207. package/dist/openapi-types.js +240 -12
  208. package/dist/openapi-types.js.map +1 -1
  209. package/dist/pipeline-report.d.ts +3 -2
  210. package/dist/pipeline-report.d.ts.map +1 -1
  211. package/dist/pipeline-report.js +19 -16
  212. package/dist/pipeline-report.js.map +1 -1
  213. package/dist/port.d.ts +5 -4
  214. package/dist/port.d.ts.map +1 -1
  215. package/dist/port.js +46 -29
  216. package/dist/port.js.map +1 -1
  217. package/dist/project-facts.d.ts +28 -4
  218. package/dist/project-facts.d.ts.map +1 -1
  219. package/dist/project-facts.js +12 -2
  220. package/dist/project-facts.js.map +1 -1
  221. package/dist/recipes/4.0.0.d.ts +9 -0
  222. package/dist/recipes/4.0.0.d.ts.map +1 -0
  223. package/dist/recipes/4.0.0.js +34 -0
  224. package/dist/recipes/4.0.0.js.map +1 -0
  225. package/dist/recipes/index.d.ts.map +1 -1
  226. package/dist/recipes/index.js +2 -0
  227. package/dist/recipes/index.js.map +1 -1
  228. package/dist/release-verification.d.ts +11 -1
  229. package/dist/release-verification.d.ts.map +1 -1
  230. package/dist/release-verification.js +47 -4
  231. package/dist/release-verification.js.map +1 -1
  232. package/dist/review.d.ts +21 -0
  233. package/dist/review.d.ts.map +1 -0
  234. package/dist/review.js +823 -0
  235. package/dist/review.js.map +1 -0
  236. package/dist/route-types.d.ts +28 -0
  237. package/dist/route-types.d.ts.map +1 -0
  238. package/dist/route-types.js +106 -0
  239. package/dist/route-types.js.map +1 -0
  240. package/dist/rules/codes.d.ts +9 -0
  241. package/dist/rules/codes.d.ts.map +1 -1
  242. package/dist/rules/codes.js +9 -0
  243. package/dist/rules/codes.js.map +1 -1
  244. package/dist/rules/data-guard.d.ts +12 -0
  245. package/dist/rules/data-guard.d.ts.map +1 -0
  246. package/dist/rules/data-guard.js +213 -0
  247. package/dist/rules/data-guard.js.map +1 -0
  248. package/dist/rules/index.d.ts +3 -0
  249. package/dist/rules/index.d.ts.map +1 -1
  250. package/dist/rules/index.js +17 -5
  251. package/dist/rules/index.js.map +1 -1
  252. package/dist/rules/islands.d.ts.map +1 -1
  253. package/dist/rules/islands.js +7 -4
  254. package/dist/rules/islands.js.map +1 -1
  255. package/dist/rules/legacy.d.ts +3 -1
  256. package/dist/rules/legacy.d.ts.map +1 -1
  257. package/dist/rules/legacy.js +829 -34
  258. package/dist/rules/legacy.js.map +1 -1
  259. package/dist/rules/nano.d.ts.map +1 -1
  260. package/dist/rules/nano.js +15 -6
  261. package/dist/rules/nano.js.map +1 -1
  262. package/dist/rules/pages.d.ts +15 -0
  263. package/dist/rules/pages.d.ts.map +1 -0
  264. package/dist/rules/pages.js +88 -0
  265. package/dist/rules/pages.js.map +1 -0
  266. package/dist/rules/routes.d.ts +11 -0
  267. package/dist/rules/routes.d.ts.map +1 -1
  268. package/dist/rules/routes.js +180 -2
  269. package/dist/rules/routes.js.map +1 -1
  270. package/dist/rules/secrets.d.ts +10 -0
  271. package/dist/rules/secrets.d.ts.map +1 -0
  272. package/dist/rules/secrets.js +169 -0
  273. package/dist/rules/secrets.js.map +1 -0
  274. package/dist/rules/security.d.ts +7 -0
  275. package/dist/rules/security.d.ts.map +1 -1
  276. package/dist/rules/security.js +171 -65
  277. package/dist/rules/security.js.map +1 -1
  278. package/dist/rules/typescript.d.ts +16 -0
  279. package/dist/rules/typescript.d.ts.map +1 -0
  280. package/dist/rules/typescript.js +59 -0
  281. package/dist/rules/typescript.js.map +1 -0
  282. package/dist/rules/zones.d.ts +17 -0
  283. package/dist/rules/zones.d.ts.map +1 -0
  284. package/dist/rules/zones.js +174 -0
  285. package/dist/rules/zones.js.map +1 -0
  286. package/dist/scaffold.d.ts +25 -12
  287. package/dist/scaffold.d.ts.map +1 -1
  288. package/dist/scaffold.js +154 -90
  289. package/dist/scaffold.js.map +1 -1
  290. package/dist/sdk.d.ts +17 -3
  291. package/dist/sdk.d.ts.map +1 -1
  292. package/dist/sdk.js +941 -153
  293. package/dist/sdk.js.map +1 -1
  294. package/dist/search-terms.d.ts +2 -0
  295. package/dist/search-terms.d.ts.map +1 -1
  296. package/dist/search-terms.js +23 -8
  297. package/dist/search-terms.js.map +1 -1
  298. package/dist/smoke.d.ts +79 -0
  299. package/dist/smoke.d.ts.map +1 -0
  300. package/dist/smoke.js +494 -0
  301. package/dist/smoke.js.map +1 -0
  302. package/dist/sync-manifest.d.ts.map +1 -1
  303. package/dist/sync-manifest.js +2 -1
  304. package/dist/sync-manifest.js.map +1 -1
  305. package/dist/sync-routes.d.ts +4 -3
  306. package/dist/sync-routes.d.ts.map +1 -1
  307. package/dist/sync-routes.js +4 -3
  308. package/dist/sync-routes.js.map +1 -1
  309. package/dist/test-tool.d.ts.map +1 -1
  310. package/dist/test-tool.js +30 -11
  311. package/dist/test-tool.js.map +1 -1
  312. package/dist/types-search.d.ts.map +1 -1
  313. package/dist/types-search.js +11 -5
  314. package/dist/types-search.js.map +1 -1
  315. package/dist/upgrade.d.ts +36 -6
  316. package/dist/upgrade.d.ts.map +1 -1
  317. package/dist/upgrade.js +167 -29
  318. package/dist/upgrade.js.map +1 -1
  319. package/dist/verification-plan.d.ts.map +1 -1
  320. package/dist/verification-plan.js +8 -0
  321. package/dist/verification-plan.js.map +1 -1
  322. package/dist/verification.d.ts.map +1 -1
  323. package/dist/verification.js +45 -12
  324. package/dist/verification.js.map +1 -1
  325. package/dist/web-app-options.d.ts +19 -0
  326. package/dist/web-app-options.d.ts.map +1 -0
  327. package/dist/web-app-options.js +49 -0
  328. package/dist/web-app-options.js.map +1 -0
  329. package/dist/work-graph.d.ts +3 -0
  330. package/dist/work-graph.d.ts.map +1 -1
  331. package/dist/work-graph.js +33 -16
  332. package/dist/work-graph.js.map +1 -1
  333. package/docs/examples.json +185 -29
  334. package/docs/llms-full.txt +4123 -548
  335. package/docs/types.json +14391 -10060
  336. package/package.json +19 -11
  337. package/src/app-files.ts +40 -0
  338. package/src/assure-hydration.ts +139 -46
  339. package/src/assure.ts +17 -2
  340. package/src/capabilities-tool.ts +35 -9
  341. package/src/cdn-check.ts +268 -0
  342. package/src/check-diagnostics.ts +199 -797
  343. package/src/check-scan.ts +394 -63
  344. package/src/check.ts +146 -58
  345. package/src/cli.ts +347 -92
  346. package/src/command-catalog.ts +407 -45
  347. package/src/config-target.ts +47 -0
  348. package/src/contracts.ts +66 -15
  349. package/src/db-audit.ts +128 -0
  350. package/src/db-child.ts +285 -0
  351. package/src/db-config.ts +224 -0
  352. package/src/db-tool.ts +690 -0
  353. package/src/dev-bun-config.ts +116 -40
  354. package/src/dev-feed-tool.ts +912 -0
  355. package/src/dev-server-client.ts +267 -0
  356. package/src/diagnostics.ts +179 -0
  357. package/src/diff-tool.ts +6 -5
  358. package/src/docs-search.ts +10 -3
  359. package/src/doctor.ts +98 -15
  360. package/src/env-file.ts +78 -1
  361. package/src/fix-recipes.ts +44 -31
  362. package/src/frontend-guidance.ts +5 -5
  363. package/src/i18n-check.ts +179 -0
  364. package/src/init-agents.ts +301 -38
  365. package/src/internal/code-unit-order.ts +5 -0
  366. package/src/internal/script-kind.ts +11 -0
  367. package/src/internal/source-facts.ts +96 -22
  368. package/src/internal/typescript-import.ts +663 -68
  369. package/src/introspect.ts +93 -82
  370. package/src/learn.ts +3 -3
  371. package/src/levels-tool.ts +47 -2
  372. package/src/load.ts +144 -32
  373. package/src/manifest-tool.ts +3 -2
  374. package/src/mcp-app-tool.ts +2 -0
  375. package/src/mcp-context.ts +5 -7
  376. package/src/mcp-delegate.ts +162 -0
  377. package/src/mcp-docs-tools.ts +23 -5
  378. package/src/mcp-exec.ts +301 -161
  379. package/src/mcp-http.ts +22 -5
  380. package/src/mcp-io.ts +130 -0
  381. package/src/mcp-isolate.ts +287 -0
  382. package/src/mcp-path.ts +50 -0
  383. package/src/mcp-project-child.ts +182 -0
  384. package/src/mcp-reflect.ts +3 -2
  385. package/src/mcp-render.ts +72 -42
  386. package/src/mcp-root.ts +130 -24
  387. package/src/mcp-run.ts +190 -56
  388. package/src/mcp-ws.ts +4 -1
  389. package/src/mcp.ts +193 -91
  390. package/src/migrate-layout.ts +1121 -0
  391. package/src/openapi-tool.ts +7 -3
  392. package/src/openapi-types.ts +315 -13
  393. package/src/pipeline-report.ts +22 -18
  394. package/src/port.ts +53 -30
  395. package/src/project-facts.ts +42 -8
  396. package/src/recipes/4.0.0.ts +35 -0
  397. package/src/recipes/index.ts +2 -0
  398. package/src/release-verification.ts +74 -5
  399. package/src/review.ts +988 -0
  400. package/src/route-types.ts +133 -0
  401. package/src/rules/codes.ts +9 -0
  402. package/src/rules/data-guard.ts +230 -0
  403. package/src/rules/index.ts +18 -5
  404. package/src/rules/islands.ts +12 -4
  405. package/src/rules/legacy.ts +947 -35
  406. package/src/rules/nano.ts +18 -6
  407. package/src/rules/pages.ts +99 -0
  408. package/src/rules/routes.ts +181 -2
  409. package/src/rules/secrets.ts +174 -0
  410. package/src/rules/security.ts +198 -70
  411. package/src/rules/typescript.ts +77 -0
  412. package/src/rules/zones.ts +195 -0
  413. package/src/scaffold.ts +175 -98
  414. package/src/sdk.ts +1215 -158
  415. package/src/search-terms.ts +24 -7
  416. package/src/smoke.ts +607 -0
  417. package/src/sync-manifest.ts +2 -1
  418. package/src/sync-routes.ts +4 -3
  419. package/src/test-tool.ts +32 -11
  420. package/src/types-search.ts +11 -4
  421. package/src/upgrade.ts +205 -25
  422. package/src/verification-plan.ts +27 -0
  423. package/src/verification.ts +50 -12
  424. package/src/web-app-options.ts +57 -0
  425. package/src/work-graph.ts +40 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nifrajs/cli",
3
- "version": "3.3.0",
3
+ "version": "4.0.0",
4
4
  "description": "The nifra CLI - zero-config dev/build/start for a nifra app (`nifra dev`, `nifra build`, `nifra start`).",
5
5
  "keywords": [
6
6
  "bun",
@@ -17,6 +17,9 @@
17
17
  "dev-server"
18
18
  ],
19
19
  "type": "module",
20
+ "nifra": {
21
+ "environment": "backend"
22
+ },
20
23
  "bin": {
21
24
  "nifra": "./dist/cli.js"
22
25
  },
@@ -66,14 +69,17 @@
66
69
  "access": "public"
67
70
  },
68
71
  "dependencies": {
69
- "@nifrajs/client": "3.3.0",
70
- "@nifrajs/core": "3.3.0",
71
- "@nifrajs/mcp": "3.3.0",
72
- "@nifrajs/runner": "3.3.0",
73
- "@nifrajs/schema": "3.3.0",
74
- "@nifrajs/testing": "3.3.0",
75
- "@nifrajs/web": "3.3.0",
76
- "create-nifra": "3.3.0"
72
+ "@nifrajs/client": "4.0.0",
73
+ "@nifrajs/core": "4.0.0",
74
+ "@nifrajs/agent-review": "4.0.0",
75
+ "@nifrajs/i18n": "4.0.0",
76
+ "@nifrajs/mcp": "4.0.0",
77
+ "@nifrajs/mcp-db": "4.0.0",
78
+ "@nifrajs/runner": "4.0.0",
79
+ "@nifrajs/schema": "4.0.0",
80
+ "@nifrajs/testing": "4.0.0",
81
+ "@nifrajs/web": "4.0.0",
82
+ "create-nifra": "4.0.0"
77
83
  },
78
84
  "repository": {
79
85
  "type": "git",
@@ -85,7 +91,7 @@
85
91
  },
86
92
  "homepage": "https://github.com/nifrajs/nifra#readme",
87
93
  "peerDependencies": {
88
- "typescript": "^5.0.0 || ^6.0.0"
94
+ "typescript": "^5.0.0 || ^6.0.0 || ^7.0.0"
89
95
  },
90
96
  "peerDependenciesMeta": {
91
97
  "typescript": {
@@ -93,6 +99,8 @@
93
99
  }
94
100
  },
95
101
  "devDependencies": {
96
- "typescript": "^6.0.3"
102
+ "typescript": "^6.0.3",
103
+ "typescript5": "npm:typescript@5.9.3",
104
+ "typescript7": "npm:typescript@7.0.2"
97
105
  }
98
106
  }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Where an app's own entry files live, in one place for every command.
3
+ *
4
+ * - `nifra.config.ts` - the CLI's config (dev/build tooling); only the CLI imports it.
5
+ * - `backend/framework.ts` - the render adapter and the options the generated server entry imports,
6
+ * kept free of build tooling because it is bundled into every server build.
7
+ * - `backend/app.ts` - exports `backend`, the app's API (a `@nifrajs/core` server). Optional.
8
+ */
9
+ import { existsSync } from "node:fs"
10
+ import { resolve } from "node:path"
11
+
12
+ export const CONFIG_FILE = "nifra.config.ts"
13
+ export const FRAMEWORK_FILE = "backend/framework.ts"
14
+ export const BACKEND_APP_FILE = "backend/app.ts"
15
+
16
+ /** The root files this layout retired, and where each lives now. */
17
+ export const RETIRED_ROOT_FILES: Readonly<Record<string, string>> = {
18
+ "backend.ts": BACKEND_APP_FILE,
19
+ "framework.ts": FRAMEWORK_FILE,
20
+ }
21
+
22
+ /** Refuse an app still on the retired root files: run against it, a command would silently miss them. */
23
+ export function assertCurrentLayout(cwd: string): void {
24
+ const retired = Object.entries(RETIRED_ROOT_FILES).filter(([file]) =>
25
+ existsSync(resolve(cwd, file)),
26
+ )
27
+ if (retired.length === 0) return
28
+ const moves = retired.map(([from, to]) => `${from} -> ${to}`).join(", ")
29
+ throw new Error(
30
+ `[nifra] this app uses the retired root layout (${moves}). Run \`nifra migrate layout\` to see the ` +
31
+ "move (it also splits each route into x.tsx and x.backend.ts), then `nifra migrate layout --write`.",
32
+ )
33
+ }
34
+
35
+ /** The file the generated server entry imports the adapter from: `backend/framework.ts` when it exists,
36
+ * else the config (a single-target app may keep everything in `nifra.config.ts`). */
37
+ export function adapterFile(cwd: string): string {
38
+ const framework = resolve(cwd, FRAMEWORK_FILE)
39
+ return existsSync(framework) ? framework : resolve(cwd, CONFIG_FILE)
40
+ }
@@ -1,9 +1,8 @@
1
1
  import { existsSync, statSync } from "node:fs"
2
- import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
2
+ import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
3
3
  import { tmpdir } from "node:os"
4
4
  import { basename, join, resolve } from "node:path"
5
5
  import { fileURLToPath, pathToFileURL } from "node:url"
6
- import { inProcessClient } from "@nifrajs/client"
7
6
  import { defineReplayFile } from "@nifrajs/core/replay"
8
7
  import { type AppLike, runApp } from "@nifrajs/runner"
9
8
  import { createWebApp, type RenderAdapter } from "@nifrajs/web"
@@ -12,6 +11,8 @@ import { discoverRoutes } from "@nifrajs/web/fs"
12
11
  import type { BunPlugin } from "bun"
13
12
  import { collectDoctorResult } from "./doctor.ts"
14
13
  import { loadApp, resolvePlugins } from "./load.ts"
14
+ import { CHILD_OUTPUT_MAX_BYTES, joinHeadTail, readBoundedStream, readHeadTail } from "./mcp-io.ts"
15
+ import { frameworkWebAppOptions } from "./web-app-options.ts"
15
16
 
16
17
  const HYDRATION_ASSURANCE = Symbol.for("nifra.hydration.assurance")
17
18
  const SOURCE_GLOB = "**/*.{ts,tsx,js,jsx,vue,svelte,mdx}"
@@ -176,6 +177,8 @@ async function buildHydrationApp(cwd: string): Promise<BuiltHydrationApp | { ski
176
177
  const client = await buildClient({
177
178
  routesDir: loaded.routesDir,
178
179
  outDir: clientOutput,
180
+ // The runner imports the entry from disk, where a `/assets/` chunk URL is a filesystem path.
181
+ publicPath: `${pathToFileURL(clientOutput).href}/`,
179
182
  clientModule: loaded.framework.clientModule,
180
183
  ...(plugins.length === 0 ? {} : { plugins: plugins as BunPlugin[] }),
181
184
  ...(loaded.framework.conditions === undefined
@@ -186,6 +189,9 @@ async function buildHydrationApp(cwd: string): Promise<BuiltHydrationApp | { ski
186
189
  ...(loaded.framework.publicEnvPrefix === undefined
187
190
  ? {}
188
191
  : { publicEnvPrefix: loaded.framework.publicEnvPrefix }),
192
+ ...(loaded.framework.secretExemptions === undefined
193
+ ? {}
194
+ : { secretExemptions: loaded.framework.secretExemptions }),
189
195
  })
190
196
  if (client.entry.trim() === "" || client.assets.length === 0) {
191
197
  await rm(workDir, { recursive: true, force: true })
@@ -200,7 +206,7 @@ async function buildHydrationApp(cwd: string): Promise<BuiltHydrationApp | { ski
200
206
  adapter: loaded.framework.adapter as RenderAdapter,
201
207
  manifest: discoverRoutes(loaded.routesDir),
202
208
  clientEntry: client.entry,
203
- ...(loaded.backend === undefined ? {} : { api: inProcessClient(loaded.backend as never) }),
209
+ ...frameworkWebAppOptions(loaded.framework, loaded.backend),
204
210
  }) as unknown as AppLike
205
211
  return {
206
212
  app: webApp,
@@ -232,21 +238,14 @@ async function sourceFiles(cwd: string): Promise<Array<{ file: string; content:
232
238
  return files
233
239
  }
234
240
 
241
+ /** How long a route may take to mark itself hydrated before the gate fails it. */
242
+ const HYDRATION_TIMEOUT_MS = 5_000
243
+ let entryCopies = 0
244
+
235
245
  function outputPath(outputDir: string, entry: string): string {
236
246
  return join(outputDir, basename(entry))
237
247
  }
238
248
 
239
- function globalValue(html: string, name: string): unknown {
240
- const expression = new RegExp(`window\\.${name}=([\\s\\S]*?)(?:</script>|;window\\.)`)
241
- const value = expression.exec(html)?.[1]?.replace(/;$/, "")
242
- if (value === undefined) return undefined
243
- try {
244
- return JSON.parse(value)
245
- } catch {
246
- return undefined
247
- }
248
- }
249
-
250
249
  async function runDomHydration(
251
250
  cwd: string,
252
251
  built: BuiltHydrationApp,
@@ -288,11 +287,21 @@ async function runDomHydration(
288
287
  else if (name === "cancelAnimationFrame") globals[name] = (id: number) => clearTimeout(id)
289
288
  else globals[name] = windowValue[name]
290
289
  }
290
+ // Framework runtimes reach for more DOM globals (Vue's `SVGElement`, `Text`, `Comment`). Lend every
291
+ // one the runtime does not define itself, so its own `fetch`, `URL` and timers stay in place.
292
+ for (const name of Object.getOwnPropertyNames(windowValue)) {
293
+ if (name in globals) continue
294
+ let value: unknown
295
+ try {
296
+ value = windowValue[name]
297
+ } catch {
298
+ continue
299
+ }
300
+ previous.set(name, undefined)
301
+ globals[name] =
302
+ typeof value === "function" && !/^[A-Z]/.test(name) ? value.bind(windowValue) : value
303
+ }
291
304
  const windowRecord = windowValue as Record<string, unknown>
292
- windowRecord.__NIFRA_ROUTE__ = globalValue(html, "__NIFRA_ROUTE__")
293
- windowRecord.__NIFRA_DATA__ = globalValue(html, "__NIFRA_DATA__")
294
- windowRecord.__NIFRA_LAYOUT_DATA__ = globalValue(html, "__NIFRA_LAYOUT_DATA__")
295
- windowRecord.__NIFRA_ACTION__ = globalValue(html, "__NIFRA_ACTION__")
296
305
  windowRecord.console = console
297
306
  const errors: string[] = []
298
307
  const recover = (error: unknown, info?: unknown): void => {
@@ -310,7 +319,12 @@ async function runDomHydration(
310
319
  documentValue.querySelector as (selector: string) => { innerHTML?: unknown } | null
311
320
  )("#root")
312
321
  const before = String(beforeRoot?.innerHTML ?? "")
313
- const entry = outputPath(built.outputDir, built.client.entry)
322
+ // Bun evaluates a file module once per path, query or not, so each route imports its own copy.
323
+ // A fresh directory each time: Bun's resolver caches a directory's listing once it has read it.
324
+ const copyDir = join(built.outputDir, `route-${++entryCopies}`)
325
+ await mkdir(copyDir)
326
+ const entry = join(copyDir, basename(built.client.entry))
327
+ await copyFile(outputPath(built.outputDir, built.client.entry), entry)
314
328
  const originalError = console.error
315
329
  const originalWarn = console.warn
316
330
  console.error = (...args) => {
@@ -323,10 +337,32 @@ async function runDomHydration(
323
337
  if (/hydr|mismatch|server|client|expected|recover/i.test(message)) errors.push(message)
324
338
  originalWarn(...args)
325
339
  }
340
+ // A render that throws on the client surfaces from a scheduler task, not from the import.
341
+ const uncaught = (error: unknown): void => {
342
+ errors.push(`uncaught: ${error instanceof Error ? error.message : String(error)}`)
343
+ }
344
+ process.on("uncaughtException", uncaught)
345
+ process.on("unhandledRejection", uncaught)
346
+ let hydrated = false
326
347
  try {
327
- await import(`${pathToFileURL(entry).href}?nifra-hydration=${encodeURIComponent(path)}`)
328
- await new Promise((resolvePromise) => setTimeout(resolvePromise, 0))
348
+ await import(pathToFileURL(entry).href)
349
+ // The entry marks <html> once the adapter has mounted the route. `Date.now` is pinned by the
350
+ // deterministic runtime, so the bound is measured with `performance.now`.
351
+ const marked = documentValue.documentElement as { hasAttribute(name: string): boolean }
352
+ const started = performance.now()
353
+ for (;;) {
354
+ hydrated = marked.hasAttribute("data-nifra-hydrated")
355
+ if (hydrated || errors.length > 0 || performance.now() - started >= HYDRATION_TIMEOUT_MS)
356
+ break
357
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 1))
358
+ }
359
+ // The framework commits the hydration in its own scheduler tasks after the marker's frame.
360
+ for (let tick = 0; tick < 10; tick++) {
361
+ await new Promise((resolvePromise) => setTimeout(resolvePromise, 0))
362
+ }
329
363
  } finally {
364
+ process.off("uncaughtException", uncaught)
365
+ process.off("unhandledRejection", uncaught)
330
366
  console.error = originalError
331
367
  console.warn = originalWarn
332
368
  }
@@ -335,6 +371,16 @@ async function runDomHydration(
335
371
  )("#root")
336
372
  const after = String(afterRoot?.innerHTML ?? "")
337
373
  const diagnostics: import("./diagnostics.ts").Diagnostic[] = []
374
+ if (!hydrated && errors.length === 0) {
375
+ diagnostics.push(
376
+ diagnostic(
377
+ "NF-H001",
378
+ `client hydration did not complete within ${HYDRATION_TIMEOUT_MS / 1000}s`,
379
+ undefined,
380
+ [built.framework],
381
+ ),
382
+ )
383
+ }
338
384
  if (errors.length > 0 || before !== after) {
339
385
  diagnostics.push(
340
386
  diagnostic("NF-H001", "client hydration reported a recoverable mismatch", undefined, [
@@ -555,57 +601,104 @@ async function runHydrationProof(cwd: string, options: HydrationOptions): Promis
555
601
  }
556
602
  }
557
603
 
604
+ /** How long one hydration run may take: it builds the client and renders every route. */
605
+ const HYDRATION_RUN_TIMEOUT_MS = 300_000
606
+
607
+ /** Bounds on a hydration run beyond its {@link HydrationOptions}. */
608
+ export interface HydrationRunControl {
609
+ readonly signal?: AbortSignal
610
+ /** Default 300000 (five minutes). */
611
+ readonly timeoutMs?: number
612
+ }
613
+
558
614
  /** Run hydration in a fresh process so the project's current client and framework modules are isolated. */
559
615
  export async function runHydrationAssurance(
560
616
  cwd: string,
561
617
  options: HydrationOptions = {},
618
+ control: HydrationRunControl = {},
562
619
  ): Promise<HydrationResult> {
620
+ const failed = (reason: string): HydrationResult => ({
621
+ diagnostics: [diagnostic("NF-H001", `hydration runner failed: ${reason}`)],
622
+ })
623
+ if (control.signal?.aborted) return failed("cancelled")
563
624
  const root = resolve(cwd)
564
625
  const entry = fileURLToPath(import.meta.url)
626
+ // The answer line carries this token, so whatever project code prints cannot be read as the result.
627
+ const token = crypto.randomUUID()
565
628
  const proc = Bun.spawn(
566
629
  [process.execPath, entry, root, "--nifra-hydration-child", JSON.stringify(options)],
567
630
  {
568
- stdin: "ignore",
631
+ cwd: root,
632
+ // Explicit: without `env`, Bun passes the environment it started with, missing `--env-file` values.
633
+ env: process.env,
634
+ stdin: "pipe",
569
635
  stdout: "pipe",
570
636
  stderr: "pipe",
571
637
  },
572
638
  )
573
- const [stdout, stderr] = await Promise.all([
574
- new Response(proc.stdout).text(),
575
- new Response(proc.stderr).text(),
576
- ])
577
- await proc.exited
639
+ let stopped: string | undefined
640
+ const stop = (reason: string): void => {
641
+ stopped ??= reason
642
+ proc.kill()
643
+ }
644
+ const onAbort = (): void => stop("cancelled")
645
+ control.signal?.addEventListener("abort", onAbort, { once: true })
646
+ const timeoutMs = control.timeoutMs ?? HYDRATION_RUN_TIMEOUT_MS
647
+ const timer = setTimeout(() => stop(`timed out after ${timeoutMs} ms`), timeoutMs)
578
648
  try {
579
- return JSON.parse(stdout) as HydrationResult
580
- } catch {
581
- return {
582
- diagnostics: [
583
- diagnostic(
584
- "NF-H001",
585
- `hydration runner failed: ${stderr.trim() || stdout.trim() || "no result"}`,
586
- ),
587
- ],
649
+ try {
650
+ proc.stdin.write(`${token}\n`)
651
+ await proc.stdin.end()
652
+ } catch {
653
+ // The child exited before reading its token; its stderr says why.
654
+ }
655
+ const [stdout, stderr] = await Promise.all([
656
+ readBoundedStream(proc.stdout, CHILD_OUTPUT_MAX_BYTES, () =>
657
+ stop(`output exceeded ${CHILD_OUTPUT_MAX_BYTES} bytes`),
658
+ ),
659
+ readHeadTail(proc.stderr, 2048, 2048),
660
+ proc.exited,
661
+ ])
662
+ if (stopped !== undefined) return failed(stopped)
663
+ const prefix = `${token} `
664
+ const answer = stdout.text.split("\n").find((line) => line.startsWith(prefix))
665
+ if (answer !== undefined) {
666
+ try {
667
+ return JSON.parse(answer.slice(prefix.length)) as HydrationResult
668
+ } catch {
669
+ // A truncated answer line falls through to the stderr report.
670
+ }
588
671
  }
672
+ return failed(joinHeadTail(stderr).trim() || "no result")
673
+ } finally {
674
+ clearTimeout(timer)
675
+ control.signal?.removeEventListener("abort", onAbort)
589
676
  }
590
677
  }
591
678
 
592
679
  if (import.meta.main && process.argv.includes("--nifra-hydration-child")) {
680
+ // Bound before project code loads, so a `console.log` in a config or loader goes to stderr and
681
+ // never reaches the answer channel.
682
+ const stdout = process.stdout.write.bind(process.stdout)
683
+ const toStderr = (...args: unknown[]): void => {
684
+ process.stderr.write(`${args.map((arg) => String(arg)).join(" ")}\n`)
685
+ }
686
+ console.log = toStderr
687
+ console.info = toStderr
688
+ console.debug = toStderr
689
+ const token = (await readBoundedStream(Bun.stdin.stream(), 256)).text.trim()
593
690
  const cwd = process.argv[2] ?? process.cwd()
594
691
  const rawOptions = process.argv[4]
692
+ let result: HydrationResult
595
693
  try {
596
694
  process.chdir(cwd)
597
695
  const options = rawOptions === undefined ? {} : (JSON.parse(rawOptions) as HydrationOptions)
598
- const result = await runHydrationProof(cwd, options)
599
- await Bun.write(Bun.stdout, JSON.stringify(result))
696
+ result = await runHydrationProof(cwd, options)
600
697
  } catch (error) {
601
- await Bun.write(
602
- Bun.stdout,
603
- JSON.stringify({
604
- diagnostics: [
605
- diagnostic("NF-H001", error instanceof Error ? error.message : String(error)),
606
- ],
607
- }),
608
- )
698
+ result = {
699
+ diagnostics: [diagnostic("NF-H001", error instanceof Error ? error.message : String(error))],
700
+ }
609
701
  }
702
+ await new Promise<void>((done) => stdout(`${token} ${JSON.stringify(result)}\n`, () => done()))
610
703
  process.exit(0)
611
704
  }
package/src/assure.ts CHANGED
@@ -4,6 +4,7 @@ import { existsSync } from "node:fs"
4
4
  import { join, resolve } from "node:path"
5
5
  import { pathToFileURL } from "node:url"
6
6
  import type { AssuranceConfig, AssuranceReport } from "@nifrajs/core/assurance"
7
+ import { CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
7
8
  import type { Diagnostic } from "./diagnostics.ts"
8
9
 
9
10
  export interface AssureSink {
@@ -92,10 +93,24 @@ export async function loadAssuranceConfig(
92
93
 
93
94
  export function formatAssuranceReport(report: AssuranceReport): string {
94
95
  if (report.ok) {
96
+ const contracted = report.routes.filter((route) => route.responseContract !== undefined)
97
+ const enforced = contracted.filter((route) => route.responseContract === "enforce").length
98
+ const warned = contracted.filter((route) => route.responseContract === "warn").length
99
+ const unchecked = contracted.filter((route) => route.responseContract === "unchecked").length
100
+ const runtimeContract =
101
+ contracted.length === 0
102
+ ? " No declared response/error schemas were found."
103
+ : " Runtime response contracts: " +
104
+ enforced +
105
+ " enforced, " +
106
+ warned +
107
+ " warn-only, " +
108
+ unchecked +
109
+ " typed-only."
95
110
  const capability = report.capabilities
96
111
  ? ` Capability assurance covered ${report.capabilities.routes.length} route${report.capabilities.routes.length === 1 ? "" : "s"}.`
97
112
  : ""
98
- return `✓ route assurance: ${report.routes.length} route${report.routes.length === 1 ? "" : "s"} classified; all required evidence is present.${capability}`
113
+ return `✓ route assurance: ${report.routes.length} route${report.routes.length === 1 ? "" : "s"} classified; all required evidence is present.${capability}${runtimeContract}`
99
114
  }
100
115
  const messages = [
101
116
  ...report.findings.map((finding) => finding.message),
@@ -280,7 +295,7 @@ export async function collectAssureBundle(
280
295
  })
281
296
 
282
297
  await addGate("render", async () => {
283
- if (!existsSync(join(cwd, "nifra.config.ts")) && !existsSync(join(cwd, "framework.ts")))
298
+ if (!existsSync(join(cwd, CONFIG_FILE)) && !existsSync(join(cwd, FRAMEWORK_FILE)))
284
299
  return { diagnostics: [], skipReason: "no client render target configured" }
285
300
  const { renderPages } = await import("./mcp-render.ts")
286
301
  const result = await renderPages(cwd, [{ path: "/" }])
@@ -16,11 +16,14 @@ import {
16
16
  snapshotCapabilities,
17
17
  validCapabilityId,
18
18
  } from "@nifrajs/core/capabilities"
19
- import { reflectRoutes } from "@nifrajs/core/reflection"
19
+ import { expandOptionalParams } from "@nifrajs/core/pattern"
20
+ import { type ReflectedMount, type ReflectedRoute, reflectRoutes } from "@nifrajs/core/reflection"
20
21
  import { scanStaticRouteText, stripComments, walkSource } from "./check.ts"
21
22
 
23
+ // Import and re-export clauses are spelled out (a default binding, then `* as ns` or a `{ … }` list)
24
+ // rather than scanned lazily, which is quadratic on a long semicolon-free module.
22
25
  const EFFECT_IMPORT =
23
- /\bimport\s+(?!type\b)(?:[^'"();]*?\bfrom\s+)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)|\brequire\s*\(\s*["']([^"']+)["']\s*\)|\bexport\s+(?!type\b)[^'";]*?\bfrom\s*["']([^"']+)["']/g
26
+ /\bimport\s*(?!type\b)(?:(?:[\w$]+\s*,?\s*)?(?:\*\s*(?:as\b\s*[\w$]+\s*)?|\{[^{}'"]*\}\s*)?from\s*)?["']([^"']+)["']|\bimport\s*\(\s*["']([^"']+)["']\s*\)|\brequire\s*\(\s*["']([^"']+)["']\s*\)|\bexport\s*(?!type\b)(?:\*\s*(?:as\b\s*[\w$]+\s*)?|\{[^{}'"]*\}\s*)from\s*["']([^"']+)["']/g
24
27
  // `\\` is excluded from both inner classes: letting the class also match a lone backslash makes the
25
28
  // `(A*(\\.A*)*)` shape ambiguous, which is exponential on a run of backslashes.
26
29
  const TEMPLATE_EFFECT_IMPORT = /\b(?:import|require)\s*\(\s*`([^`$\\]*(?:\\.[^`$\\]*)*)`\s*\)/g
@@ -312,27 +315,44 @@ async function walkRouteModules(
312
315
  return { covered, evidence, violations, truncations }
313
316
  }
314
317
 
318
+ /** The reflected paths a statically collected pattern stands for; a malformed pattern is itself. */
319
+ function staticRouteForms(path: string): readonly string[] {
320
+ try {
321
+ return expandOptionalParams(path)
322
+ } catch {
323
+ return [path]
324
+ }
325
+ }
326
+
315
327
  /** Build coverage-qualified static evidence for every reflected route. */
316
328
  export async function collectCapabilityProjectReport(
317
329
  cwd: string,
318
330
  source: unknown,
319
331
  policyInput: CapabilityPolicy,
332
+ options: {
333
+ readonly routes?: readonly ReflectedRoute[]
334
+ readonly mounts?: readonly ReflectedMount[]
335
+ } = {},
320
336
  ): Promise<CapabilityProjectReport> {
321
337
  const policy = defineCapabilityPolicy(policyInput)
322
338
  const sources = await readSources(cwd)
323
339
  const automatic = new Map<string, Set<string>>()
324
340
  for (const [file, content] of sources) {
325
341
  for (const route of scanStaticRouteText(file, content)) {
326
- const modules = automatic.get(routeKey(route.method, route.path)) ?? new Set<string>()
327
- modules.add(file)
328
- automatic.set(routeKey(route.method, route.path), modules)
342
+ // A path ending in optional params is reflected as one route per form it serves.
343
+ for (const path of staticRouteForms(route.path)) {
344
+ const modules = automatic.get(routeKey(route.method, path)) ?? new Set<string>()
345
+ modules.add(file)
346
+ automatic.set(routeKey(route.method, path), modules)
347
+ }
329
348
  }
330
349
  }
331
350
 
332
351
  const evidenceRoutes = []
333
352
  const violations: CapabilityImportViolation[] = []
334
353
  const truncations: CapabilityProvenanceTruncation[] = []
335
- for (const route of reflectRoutes(source)) {
354
+ const reflectedRoutes = options.routes ?? reflectRoutes(source)
355
+ for (const route of reflectedRoutes) {
336
356
  const modules = new Set(automatic.get(routeKey(route.method, route.path)) ?? [])
337
357
  for (const association of policy.provenance.routeModules ?? []) {
338
358
  if (
@@ -394,7 +414,11 @@ export async function collectCapabilityProjectReport(
394
414
  }
395
415
  }
396
416
 
397
- const evaluated = evaluateCapabilityAssurance(source, policy, { routes: evidenceRoutes })
417
+ const evaluated = evaluateCapabilityAssurance(source, policy, {
418
+ routes: evidenceRoutes,
419
+ reflectedRoutes,
420
+ ...(options.mounts === undefined ? {} : { mounts: options.mounts }),
421
+ })
398
422
  const report: CapabilityAssuranceReport =
399
423
  violations.length === 0 && truncations.length === 0 && unmatchedSeams.length === 0
400
424
  ? evaluated
@@ -674,9 +698,11 @@ export async function runCapabilityCheck(
674
698
  2,
675
699
  ),
676
700
  )
677
- } else if (ok) {
678
- console.log("✓ capability assurance and lockfile are current")
679
701
  } else {
702
+ if (ok) console.log("✓ capability assurance and lockfile are current")
703
+ // Stated on every run, passing or not: a gap is the report's own boundary, not a failure.
704
+ for (const gap of result.report.gaps ?? [])
705
+ console.log(`• known gap, not analyzed: ${gap.path} - ${gap.reason}`)
680
706
  for (const finding of result.report.findings) console.log(`✖ ${finding.message}`)
681
707
  for (const violation of result.violations)
682
708
  console.log(