@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/src/introspect.ts CHANGED
@@ -7,74 +7,25 @@
7
7
 
8
8
  import { reservedKeyFor } from "@nifrajs/client"
9
9
  import {
10
- type ProjectEvidenceSchema,
11
10
  type ProjectEvidenceSchemaPart,
12
11
  type ProjectEvidenceSnapshot,
12
+ reflectedRoutesFromEvidence,
13
13
  snapshotProjectEvidence,
14
14
  } from "@nifrajs/core/evidence"
15
- import type { ReflectedRoute, SchemaReflection } from "@nifrajs/core/reflection"
15
+ import { compileRoutePattern, type RoutePatternSegment } from "@nifrajs/core/pattern"
16
+ import type { ReflectedRoute } from "@nifrajs/core/reflection"
16
17
  import type { Manifest } from "@nifrajs/web"
17
18
  import { discoverRoutes } from "@nifrajs/web/fs"
19
+ import { normalizeMountPath } from "@nifrajs/web/route-manifest"
20
+ import { codeUnitOrder } from "./internal/code-unit-order.ts"
18
21
  import type { LoadedApp } from "./load.ts"
19
22
  import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.ts"
20
23
 
21
24
  /** Read the backend's registered routes, if it's a `server()` with a `.routes()` method. */
22
- export function backendRoutes(backend: unknown): ReflectedRoute[] {
25
+ export function backendRoutes(backend: unknown): readonly ReflectedRoute[] {
23
26
  return reflectedRoutesFromEvidence(snapshotProjectEvidence(backend))
24
27
  }
25
28
 
26
- function schemaPartToReflection(
27
- value: ProjectEvidenceSchemaPart | undefined,
28
- ): SchemaReflection | undefined {
29
- return value === undefined
30
- ? undefined
31
- : { standard: undefined, jsonSchema: value.jsonSchema, fields: value.fields }
32
- }
33
-
34
- function schemaFromEvidence(schema: ProjectEvidenceSchema | undefined): ReflectedRoute["schema"] {
35
- if (schema === undefined) return undefined
36
- const errors = Object.fromEntries(
37
- Object.entries(schema.errors ?? {}).map(([status, value]) => [
38
- status,
39
- schemaPartToReflection(value) as SchemaReflection,
40
- ]),
41
- )
42
- const headers = schemaPartToReflection(schema.headers)
43
- const body = schemaPartToReflection(schema.body)
44
- const query = schemaPartToReflection(schema.query)
45
- const params = schemaPartToReflection(schema.params)
46
- const response = schemaPartToReflection(schema.response)
47
- const sse = schemaPartToReflection(schema.sse)
48
- return {
49
- ...(schema.bodyLimit !== undefined ? { bodyLimit: schema.bodyLimit } : {}),
50
- ...(schema.bodyLimitReason !== undefined ? { bodyLimitReason: schema.bodyLimitReason } : {}),
51
- ...(headers !== undefined ? { headers } : {}),
52
- ...(body !== undefined ? { body } : {}),
53
- ...(query !== undefined ? { query } : {}),
54
- ...(params !== undefined ? { params } : {}),
55
- ...(response !== undefined ? { response } : {}),
56
- ...(Object.keys(errors).length > 0 ? { errors } : {}),
57
- ...(sse !== undefined ? { sse } : {}),
58
- }
59
- }
60
-
61
- /** Adapt the canonical token-only snapshot to the legacy reflection view used by Markdown helpers. */
62
- export function reflectedRoutesFromEvidence(evidence: ProjectEvidenceSnapshot): ReflectedRoute[] {
63
- return evidence.routes.map((route) => {
64
- const schema = schemaFromEvidence(route.schema)
65
- return {
66
- method: route.method,
67
- path: route.path,
68
- ...(schema !== undefined ? { schema } : {}),
69
- ...(route.assurance !== undefined ? { assurance: route.assurance } : {}),
70
- ...(route.capabilities !== undefined ? { capabilities: route.capabilities } : {}),
71
- ...(route.family === true ? { family: true } : {}),
72
- ...(route.classification !== undefined ? { classification: route.classification } : {}),
73
- ...(route.tool !== undefined ? { tool: route.tool } : {}),
74
- }
75
- })
76
- }
77
-
78
29
  interface JsonSchemaNode {
79
30
  readonly type?: unknown
80
31
  readonly properties?: Record<string, unknown>
@@ -154,6 +105,44 @@ function schemaLines(schema: ReflectedRoute["schema"]): string[] {
154
105
 
155
106
  const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/
156
107
 
108
+ /** The verbs the typed client has a terminal call for. */
109
+ const TYPED_VERBS: ReadonlySet<string> = new Set([
110
+ "get",
111
+ "post",
112
+ "put",
113
+ "patch",
114
+ "delete",
115
+ "head",
116
+ "options",
117
+ ])
118
+
119
+ /**
120
+ * The call a non-static segment adds to the chain, or `undefined` for a static one. The segment is
121
+ * read by the router's own grammar, so the spelling matches what the typed client accepts:
122
+ * - a whole param or a wildcard is called by its name - `:id` and `:id{[0-9]+}` are `({ id })`;
123
+ * - a segment that is part literal, part parameter is called with the segment text -
124
+ * `:name.json` is ``(`${name}.json`)``.
125
+ */
126
+ function paramCall(seg: string): string | undefined {
127
+ let segment: RoutePatternSegment | undefined
128
+ try {
129
+ segment = compileRoutePattern(`/${seg}`).segments[0]
130
+ } catch {
131
+ return undefined
132
+ }
133
+ if (segment === undefined || segment.kind === "static") return undefined
134
+ if (segment.kind === "param") return `({ ${segment.name} })`
135
+ if (segment.kind === "wildcard") {
136
+ return segment.name === "*" ? '({ "*": rest })' : `({ ${segment.name} })`
137
+ }
138
+ const [only] = segment.parts
139
+ if (segment.parts.length === 1 && only?.t === "param") return `({ ${only.name} })`
140
+ const text = segment.parts
141
+ .map((part) => (part.t === "lit" ? part.v.replace(/[`\\]|\$\{/g, "\\$&") : `\${${part.name}}`))
142
+ .join("")
143
+ return `(\`${text}\`)`
144
+ }
145
+
157
146
  /**
158
147
  * The typed-client proxy chain for a path, without the terminal verb call. Shared by `clientCall`
159
148
  * and the `nifra routes` collision annotation so both teach exactly one spelling.
@@ -163,10 +152,9 @@ function clientChain(path: string): string {
163
152
  if (segs.length === 0) return "api.index"
164
153
  let chain = "api"
165
154
  for (const seg of segs) {
166
- if (seg.startsWith(":") || seg.startsWith("*")) {
167
- const name = seg.replace(/^[:*]/, "") || "value"
168
- chain += `({ ${name} })`
169
- } else if (reservedKeyFor(seg) !== undefined) chain += `(${JSON.stringify(seg)})`
155
+ const call = paramCall(seg)
156
+ if (call !== undefined) chain += call
157
+ else if (reservedKeyFor(seg) !== undefined) chain += `(${JSON.stringify(seg)})`
170
158
  else chain += IDENT.test(seg) ? `.${seg}` : `[${JSON.stringify(seg)}]`
171
159
  }
172
160
  return chain
@@ -176,7 +164,8 @@ function clientChain(path: string): string {
176
164
  * The typed-client call form for a route - the exact `client<typeof app>` proxy chain an agent should
177
165
  * write, derived from the same convention `@nifrajs/client` implements (so it never has to read the
178
166
  * client tests to learn it): a static segment is a property (`.users`), a path param/wildcard is a call
179
- * that appends the value (`({ id })`), the root path is `.index`, and the HTTP verb is the terminal call.
167
+ * that appends the value (`({ id })`), a segment that is part literal, part parameter is a call with
168
+ * the segment text (``(`${name}.json`)``), the root path is `.index`, and the HTTP verb is the terminal call.
180
169
  * Body verbs (POST/PUT/PATCH) take the body first then call-options; other verbs take call-options first -
181
170
  * so the `{ query }` argument lands in the right slot for each.
182
171
  *
@@ -188,6 +177,9 @@ function clientChain(path: string): string {
188
177
  export function clientCall(method: string, path: string, schema: unknown): string {
189
178
  const s = schema as { body?: unknown; query?: unknown } | undefined
190
179
  const verb = method.toLowerCase()
180
+ // The typed client has a call for the standard methods only; any other method goes out through
181
+ // `fetch`, against the route's own path.
182
+ if (!TYPED_VERBS.has(verb)) return `await fetch(url, { method: ${JSON.stringify(method)} })`
191
183
  const chain = clientChain(path)
192
184
  const isBodyVerb = verb === "post" || verb === "put" || verb === "patch"
193
185
  let call: string
@@ -205,16 +197,18 @@ export function clientCall(method: string, path: string, schema: unknown): strin
205
197
  /** Markdown section listing the backend's API routes with their request + response field shapes. */
206
198
  export function apiRoutesSection(routes: readonly ReflectedRoute[]): string {
207
199
  if (routes.length === 0) {
208
- return "## API routes\n\nNo `backend.ts` server routes found (this app may be frontend-only)."
200
+ return "## API routes\n\nNo `backend/app.ts` server routes found (this app may be frontend-only)."
209
201
  }
210
202
  const lines = [...routes]
211
- .sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method))
203
+ .sort((a, b) => codeUnitOrder(a.path, b.path) || codeUnitOrder(a.method, b.method))
212
204
  .flatMap((r) => [
213
205
  `- \`${r.method} ${r.path}\``,
214
206
  ...schemaLines(r.schema),
215
- ` - call: \`${clientCall(r.method, r.path, r.schema)}\` → \`{ ok, status, data, error }\``,
207
+ TYPED_VERBS.has(r.method.toLowerCase())
208
+ ? ` - call: \`${clientCall(r.method, r.path, r.schema)}\` → \`{ ok, status, data, error }\``
209
+ : ` - call: \`${clientCall(r.method, r.path, r.schema)}\` (no typed-client call for this method; \`url\` is the path above)`,
216
210
  ])
217
- return `## API routes (backend.ts)\n\nEach route's \`body\`/\`query\`/\`response\` shape is its contract - the typed client derives request inputs and \`res.data\` from these, so a screen built on \`client<typeof app>\` stays in sync automatically. The \`call\` line is the exact \`client<typeof app>\` form: static path segments are properties, a path param is a call (\`({ id })\`), the verb is the terminal call (body first for POST/PUT/PATCH), and every call returns the never-throwing \`{ ok, status, data, error }\` Result.\n\n${lines.join("\n")}`
211
+ return `## API routes (backend/app.ts)\n\nEach route's \`body\`/\`query\`/\`response\` shape is its contract - the typed client derives request inputs and \`res.data\` from these, so a screen built on \`client<typeof app>\` stays in sync automatically. The \`call\` line is the exact \`client<typeof app>\` form: static path segments are properties, a path param is a call (\`({ id })\`), the verb is the terminal call (body first for POST/PUT/PATCH), and every call returns the never-throwing \`{ ok, status, data, error }\` Result.\n\n${lines.join("\n")}`
218
212
  }
219
213
 
220
214
  /** Compact API-routes INDEX for the no-arg `nifra_context` call - `METHOD path` per route, WITHOUT the
@@ -223,12 +217,12 @@ export function apiRoutesSection(routes: readonly ReflectedRoute[]): string {
223
217
  * contract via the `path`/`kind` slice, instead of every schema up front. */
224
218
  export function apiRoutesIndexSection(routes: readonly ReflectedRoute[]): string {
225
219
  if (routes.length === 0) {
226
- return "## API routes (backend.ts)\n\nNo `backend.ts` server routes found (this app may be frontend-only)."
220
+ return "## API routes (backend/app.ts)\n\nNo `backend/app.ts` server routes found (this app may be frontend-only)."
227
221
  }
228
222
  const lines = [...routes]
229
- .sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method))
223
+ .sort((a, b) => codeUnitOrder(a.path, b.path) || codeUnitOrder(a.method, b.method))
230
224
  .map((r) => `- \`${r.method} ${r.path}\``)
231
- return `## API routes (backend.ts)\n\n${routes.length} route${routes.length === 1 ? "" : "s"}. Call \`nifra_context\` again with \`path\` (a route prefix) and/or \`kind: "api"\` for the body/query/response contracts + the exact \`client<typeof app>\` call form - or \`nifra_routes\` for the same as structured JSON.\n\n${lines.join("\n")}`
225
+ return `## API routes (backend/app.ts)\n\n${routes.length} route${routes.length === 1 ? "" : "s"}. Call \`nifra_context\` again with \`path\` (a route prefix) and/or \`kind: "api"\` for the body/query/response contracts + the exact \`client<typeof app>\` call form - or \`nifra_routes\` for the same as structured JSON.\n\n${lines.join("\n")}`
232
226
  }
233
227
 
234
228
  /** Markdown section listing the file-routed pages (URL pattern → source file). */
@@ -237,7 +231,7 @@ export function pageRoutesSection(manifest: Manifest | undefined): string {
237
231
  return "## Page routes\n\nNo file routes found under `routes/`."
238
232
  }
239
233
  const lines = [...manifest.routes]
240
- .sort((a, b) => a.pattern.localeCompare(b.pattern))
234
+ .sort((a, b) => codeUnitOrder(a.pattern, b.pattern))
241
235
  .map((r) => `- \`${r.pattern}\` → \`${r.file}\``)
242
236
  return `## Page routes (routes/)\n\n${lines.join("\n")}`
243
237
  }
@@ -259,9 +253,9 @@ const CONVENTIONS = `## Conventions (summary)
259
253
  \`res.data\` from the backend's route types, so the compiler catches any frontend/backend drift. Never
260
254
  hand-roll \`fetch\` + ad-hoc response types for an internal API - that's exactly how screens drift. It
261
255
  never throws: branch on \`res.ok ? res.data : res.error\`.
262
- - **Pages:** file-routed under \`routes/\`; \`loader\`/\`action\` are server-only but the module is also bundled
263
- for the browser - **never top-level-import server-only code** (DB, secrets, \`process.env\`) into a route
264
- file; reach it via \`ctx.api\` / \`ctx.env\`.
256
+ - **Pages:** file-routed under \`routes/\`. A route is two files: \`x.tsx\` (browser) and \`x.backend.ts\`
257
+ (\`loader\`, \`action\`, \`middleware\`; server only). Server code lives under \`backend/\`, code both sides
258
+ use under \`shared/\`, components under \`frontend/\`; the browser build refuses backend code outright.
265
259
  - \`app.fetch(Request)\` is the universal entry. Full reference: this repo's \`AGENTS.md\`, or \`llms-full.txt\`.`
266
260
 
267
261
  /** Optional narrowing for {@link describeProject} - a path prefix and/or one section. A filtered
@@ -355,7 +349,7 @@ export function routesToJsonFromEvidence(
355
349
  routes = routes.filter((r) => r.path.startsWith(pathPrefix))
356
350
  }
357
351
  return [...routes]
358
- .sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method))
352
+ .sort((a, b) => codeUnitOrder(a.path, b.path) || codeUnitOrder(a.method, b.method))
359
353
  .map((r) => {
360
354
  const s = r.schema
361
355
  const body = shape(s?.body)
@@ -425,12 +419,16 @@ export interface RouteTableInput {
425
419
  /** The page router's `apiPrefix` (default `/api`); an API route at/under it is auto-mounted. `""`
426
420
  * disables the auto-mount, so no API route is marked auto-mounted. */
427
421
  readonly apiPrefix?: string
422
+ /** The backend declares its routes without the prefix and is served at `apiPrefix + path`: every API
423
+ * route is auto-mounted, listed at the path a request uses. */
424
+ readonly apiStrip?: boolean
428
425
  }
429
426
 
430
427
  /** True when `path` is at or under `prefix` as a path segment boundary (`/api` matches `/api` and
431
428
  * `/api/x`, but not `/apiary`). An empty prefix matches nothing (auto-mount disabled). */
432
429
  function isUnderPrefix(path: string, prefix: string): boolean {
433
430
  if (prefix === "") return false
431
+ if (prefix === "/") return true
434
432
  return path === prefix || path.startsWith(`${prefix}/`)
435
433
  }
436
434
 
@@ -442,7 +440,14 @@ function isUnderPrefix(path: string, prefix: string): boolean {
442
440
  * joined methods, for stable output.
443
441
  */
444
442
  export function buildRouteTable(input: RouteTableInput): RouteTableEntry[] {
445
- const apiPrefix = input.apiPrefix ?? "/api"
443
+ const configured = input.apiPrefix ?? "/api"
444
+ // Read the prefix as the server's mount table does (`/api/` and `/api/*` are `/api`).
445
+ const apiPrefix = configured === "" ? "" : (normalizeMountPath(configured) ?? configured)
446
+ const stripped = input.apiStrip === true && apiPrefix !== ""
447
+ const servedPath = (path: string): string => {
448
+ if (!stripped || apiPrefix === "/") return path
449
+ return path === "/" ? apiPrefix : `${apiPrefix}${path}`
450
+ }
446
451
  const rows: RouteTableEntry[] = []
447
452
  for (const page of input.pages) {
448
453
  const methods = page.hasAction ? ["GET", "POST"] : ["GET"]
@@ -451,11 +456,12 @@ export function buildRouteTable(input: RouteTableInput): RouteTableEntry[] {
451
456
  // Collapse API routes that share a path into one row with all its methods (a REST resource).
452
457
  const byPath = new Map<string, { methods: Set<string>; autoMounted: boolean }>()
453
458
  for (const r of input.api) {
454
- const existing = byPath.get(r.path)
459
+ const path = servedPath(r.path)
460
+ const existing = byPath.get(path)
455
461
  if (existing === undefined) {
456
- byPath.set(r.path, {
462
+ byPath.set(path, {
457
463
  methods: new Set([r.method.toUpperCase()]),
458
- autoMounted: isUnderPrefix(r.path, apiPrefix),
464
+ autoMounted: stripped || isUnderPrefix(r.path, apiPrefix),
459
465
  })
460
466
  } else existing.methods.add(r.method.toUpperCase())
461
467
  }
@@ -469,9 +475,9 @@ export function buildRouteTable(input: RouteTableInput): RouteTableEntry[] {
469
475
  }
470
476
  return rows.sort(
471
477
  (a, b) =>
472
- a.path.localeCompare(b.path) ||
473
- a.kind.localeCompare(b.kind) ||
474
- a.methods.join(",").localeCompare(b.methods.join(",")),
478
+ codeUnitOrder(a.path, b.path) ||
479
+ codeUnitOrder(a.kind, b.kind) ||
480
+ codeUnitOrder(a.methods.join(","), b.methods.join(",")),
475
481
  )
476
482
  }
477
483
 
@@ -499,7 +505,7 @@ export function clientSpellingFor(
499
505
  /** Render the route table as a terse aligned text table (the `nifra routes` default output). Pure. */
500
506
  export function renderRouteTable(rows: readonly RouteTableEntry[]): string {
501
507
  if (rows.length === 0)
502
- return "No routes found (no `routes/` pages and no `backend.ts` API routes)."
508
+ return "No routes found (no `routes/` pages and no `backend/app.ts` API routes)."
503
509
  const display = rows.map((r) => ({
504
510
  methods: r.methods.join(", "),
505
511
  kind: r.kind,
@@ -595,7 +601,7 @@ function parentPath(path: string): string | undefined {
595
601
  /** Build a stable parent/child graph from the public route table. Pure and topology-neutral. */
596
602
  export function buildRouteGraph(rows: readonly RouteTableEntry[]): RouteGraph {
597
603
  const sorted = [...rows].sort(
598
- (a, b) => a.path.localeCompare(b.path) || a.kind.localeCompare(b.kind),
604
+ (a, b) => codeUnitOrder(a.path, b.path) || codeUnitOrder(a.kind, b.kind),
599
605
  )
600
606
  const nodes: RouteGraphNode[] = [
601
607
  { id: "root", kind: "root", path: "/" },
@@ -695,7 +701,12 @@ async function collectRouteTable(app: LoadedApp): Promise<RouteTableEntry[]> {
695
701
  }
696
702
  pages.push({ pattern: route.pattern, file: route.file, hasAction })
697
703
  }
698
- const rows = buildRouteTable({ pages, api: backendRoutes(app.backend) })
704
+ const rows = buildRouteTable({
705
+ pages,
706
+ api: backendRoutes(app.backend),
707
+ ...(app.framework.apiPrefix !== undefined ? { apiPrefix: app.framework.apiPrefix } : {}),
708
+ ...(app.framework.apiStrip !== undefined ? { apiStrip: app.framework.apiStrip } : {}),
709
+ })
699
710
  return rows
700
711
  }
701
712
 
package/src/learn.ts CHANGED
@@ -39,7 +39,7 @@ export const LEARN_PATH: readonly LearnStep[] = [
39
39
  id: "page-route",
40
40
  title: "Add a page route",
41
41
  goal: "A second page, server-rendered, reachable by URL.",
42
- do: "Ask nifra_scaffold for the path (e.g. /about) - it returns the exact `routes/` file and a contract-correct page stub. File-based: `routes/about.tsx` serves `/about`.",
42
+ do: "Ask nifra_scaffold for the path (e.g. /about) - it writes the route pair: the page `routes/about.tsx` (serves `/about`) and its backend half `routes/about.backend.ts`.",
43
43
  verify: "nifra_render /about returns the rendered HTML.",
44
44
  tools: ["nifra_scaffold", "nifra_render"],
45
45
  seeAlso: "page routes file-based routing layouts",
@@ -48,7 +48,7 @@ export const LEARN_PATH: readonly LearnStep[] = [
48
48
  id: "loader",
49
49
  title: "Load data for a page",
50
50
  goal: "A page whose data is fetched on the server before render.",
51
- do: "Export a `loader` from the route and read its result in the component. Get the exact shape from nifra_example ('loader').",
51
+ do: "Export a `loader` and its `loaderOutput` schema from the route's `.backend.ts` half and read the result as `data` (`Route.ComponentProps`) in the page. Get the exact shape from nifra_example ('loader').",
52
52
  verify: "nifra_render the page and confirm the loaded data is in the HTML.",
53
53
  tools: ["nifra_example", "nifra_render"],
54
54
  seeAlso: "loader server data page",
@@ -57,7 +57,7 @@ export const LEARN_PATH: readonly LearnStep[] = [
57
57
  id: "api-route",
58
58
  title: "Add a typed API route",
59
59
  goal: "A backend endpoint with a typed request/response contract.",
60
- do: "Add the route to `backend.ts` with an @nifrajs/schema body/query/response schema. nifra_context shows the resulting contract and the exact typed-client call form.",
60
+ do: "Add the route to `backend/app.ts` with an @nifrajs/schema body/query/response schema. nifra_context shows the resulting contract and the exact typed-client call form.",
61
61
  verify:
62
62
  "nifra_run { method, path, body } and check the status + parsed body; nifra_check confirms the contract typechecks.",
63
63
  tools: ["nifra_context", "nifra_run", "nifra_check"],
@@ -36,6 +36,17 @@ export interface VerificationLevelsResult {
36
36
  /** Highest level whose whole ladder holds; -1 when even L0 fails. */
37
37
  readonly achieved: number
38
38
  readonly levels: readonly VerificationLevelStatus[]
39
+ /** Response/error runtime enforcement is reported separately from the cumulative L0-L4 ladder. */
40
+ readonly responseContracts: ResponseContractStatus
41
+ }
42
+
43
+ export interface ResponseContractStatus {
44
+ readonly declared: number
45
+ readonly enforced: number
46
+ readonly warnOnly: number
47
+ readonly typedOnly: number
48
+ readonly status: "none" | "partial" | "enforced"
49
+ readonly reasons: readonly string[]
39
50
  }
40
51
 
41
52
  export interface CollectLevelsOptions {
@@ -82,7 +93,7 @@ export async function collectVerificationLevels(
82
93
  ] as const) {
83
94
  statuses.push({ level, name, ok: false, reasons: [reason] })
84
95
  }
85
- return finalize(statuses)
96
+ return finalize(statuses, [])
86
97
  }
87
98
  const config = verification.config
88
99
 
@@ -197,11 +208,12 @@ export async function collectVerificationLevels(
197
208
  reasons: l4Reasons,
198
209
  })
199
210
 
200
- return finalize(statuses)
211
+ return finalize(statuses, assurance.routes)
201
212
  }
202
213
 
203
214
  function finalize(
204
215
  statuses: readonly { level: number; name: string; ok: boolean; reasons: string[] }[],
216
+ routes: readonly { readonly responseContract?: "enforce" | "warn" | "unchecked" }[],
205
217
  ): VerificationLevelsResult {
206
218
  // Cumulative: the achieved level is the last rung with every rung below it also holding.
207
219
  let achieved = -1
@@ -209,6 +221,31 @@ function finalize(
209
221
  if (!status.ok) break
210
222
  achieved = status.level
211
223
  }
224
+ const declared = routes.filter((route) => route.responseContract !== undefined)
225
+ const enforced = declared.filter((route) => route.responseContract === "enforce").length
226
+ const warnOnly = declared.filter((route) => route.responseContract === "warn").length
227
+ const typedOnly = declared.filter((route) => route.responseContract === "unchecked").length
228
+ const responseContracts: ResponseContractStatus = {
229
+ declared: declared.length,
230
+ enforced,
231
+ warnOnly,
232
+ typedOnly,
233
+ status:
234
+ declared.length === 0 ? "none" : typedOnly === 0 && warnOnly === 0 ? "enforced" : "partial",
235
+ reasons:
236
+ declared.length === 0
237
+ ? ["no response/error schemas declared"]
238
+ : [
239
+ ...(typedOnly > 0
240
+ ? [
241
+ `${typedOnly} declared route(s) are typed-only; install responseContract("enforce")`,
242
+ ]
243
+ : []),
244
+ ...(warnOnly > 0
245
+ ? [`${warnOnly} declared route(s) are warn-only and still send the original payload`]
246
+ : []),
247
+ ],
248
+ }
212
249
  return Object.freeze({
213
250
  achieved,
214
251
  levels: Object.freeze(
@@ -216,6 +253,10 @@ function finalize(
216
253
  Object.freeze({ ...status, reasons: Object.freeze([...status.reasons]) }),
217
254
  ),
218
255
  ),
256
+ responseContracts: Object.freeze({
257
+ ...responseContracts,
258
+ reasons: Object.freeze([...responseContracts.reasons]),
259
+ }),
219
260
  })
220
261
  }
221
262
 
@@ -233,6 +274,10 @@ export async function runLevels(
233
274
  console.log(`${mark} L${status.level} ${status.name}`)
234
275
  for (const reason of status.reasons) console.log(` - ${reason}`)
235
276
  }
277
+ console.log(
278
+ `[nifra] response contracts: ${result.responseContracts.status} (${result.responseContracts.enforced} enforced, ${result.responseContracts.warnOnly} warn-only, ${result.responseContracts.typedOnly} typed-only)`,
279
+ )
280
+ for (const reason of result.responseContracts.reasons) console.log(` - ${reason}`)
236
281
  console.log(
237
282
  result.achieved < 0
238
283
  ? "[nifra] verification level: none (L0 failing)"