@jimhoyd/urlcode 0.3.0 → 0.4.0-alpha.2

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 (380) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +122 -0
  2. package/.claude/skills/urlcode-operations/SKILL.md +108 -0
  3. package/.claude-plugin/marketplace.json +18 -0
  4. package/CONTRIBUTING.md +30 -2
  5. package/README.md +195 -255
  6. package/ROADMAP.md +143 -15
  7. package/SECURITY.md +31 -9
  8. package/dist/BUILD-MANIFEST.json +72 -47
  9. package/dist/adapters.js +4 -23
  10. package/dist/agent-lists.js +1 -1
  11. package/dist/agents-guide.js +113 -0
  12. package/dist/authoring-files.js +60 -0
  13. package/dist/authoring.js +11 -1
  14. package/dist/aws.js +4 -3
  15. package/dist/build-cloudflare.js +11 -24
  16. package/dist/build-static.js +134 -0
  17. package/dist/bulk.js +37 -0
  18. package/dist/capabilities.js +262 -0
  19. package/dist/capability-query.js +71 -0
  20. package/dist/catalog.js +105 -0
  21. package/dist/cli.js +165 -34
  22. package/dist/client-address.js +1 -1
  23. package/dist/compliance-rules/baseline.js +9 -17
  24. package/dist/compliance-rules/privacy.js +7 -18
  25. package/dist/compliance-rules/shared.js +0 -2
  26. package/dist/compliance-rules/strict.js +5 -5
  27. package/dist/compliance.js +6 -8
  28. package/dist/conditions.js +88 -0
  29. package/dist/config.js +69 -6
  30. package/dist/context.js +155 -0
  31. package/dist/ecosystem-cli.js +88 -0
  32. package/dist/egress.js +98 -0
  33. package/dist/examples.js +92 -0
  34. package/dist/explain-cli.js +64 -0
  35. package/dist/explain.js +131 -0
  36. package/dist/extensions.js +231 -0
  37. package/dist/function-sources.js +49 -5
  38. package/dist/function-worker.js +3 -1
  39. package/dist/functions.js +84 -13
  40. package/dist/guest-api.js +29 -3
  41. package/dist/index.js +40 -6
  42. package/dist/init-with.js +165 -0
  43. package/dist/interchange-cli.js +42 -0
  44. package/dist/interchange.js +189 -0
  45. package/dist/manifest.js +109 -0
  46. package/dist/match.js +2 -2
  47. package/dist/mcp-authoring.js +147 -0
  48. package/dist/mcp.js +97 -0
  49. package/dist/observability.js +7 -21
  50. package/dist/operator-host.js +29 -0
  51. package/dist/plugins.js +12 -0
  52. package/dist/policies/agents.js +2 -2
  53. package/dist/policies/cache.js +8 -3
  54. package/dist/policies/compression.js +2 -1
  55. package/dist/policies/security.js +0 -0
  56. package/dist/policies.js +1 -1
  57. package/dist/policy.js +56 -15
  58. package/dist/prerender.js +100 -41
  59. package/dist/project-tests.js +3 -3
  60. package/dist/provider-verification.js +92 -0
  61. package/dist/proxy.js +44 -0
  62. package/dist/readiness.js +34 -11
  63. package/dist/recipes.js +41 -0
  64. package/dist/route-diff.js +106 -0
  65. package/dist/router.js +45 -7
  66. package/dist/runtime.js +164 -64
  67. package/dist/sandbox.js +48 -0
  68. package/dist/scaffold.js +0 -0
  69. package/dist/schema-query.js +62 -0
  70. package/dist/scripts/operational-drills.js +12 -54
  71. package/dist/server.js +3 -29
  72. package/dist/signals.js +24 -0
  73. package/dist/site.js +0 -0
  74. package/dist/tooling.js +96 -0
  75. package/dist/trusted-functions.js +210 -0
  76. package/dist/types/adapters.d.ts +7 -4
  77. package/dist/types/agent-lists.d.ts +0 -1
  78. package/dist/types/agents-guide.d.ts +17 -0
  79. package/dist/types/authoring-files.d.ts +10 -0
  80. package/dist/types/aws.d.ts +3 -1
  81. package/dist/types/build-cloudflare.d.ts +1 -0
  82. package/dist/types/build-static.d.ts +43 -0
  83. package/dist/types/bulk.d.ts +27 -0
  84. package/dist/types/capabilities.d.ts +64 -0
  85. package/dist/types/capability-query.d.ts +24 -0
  86. package/dist/types/catalog.d.ts +65 -0
  87. package/dist/types/client-address.d.ts +0 -1
  88. package/dist/types/compliance-rules/baseline.d.ts +1 -9
  89. package/dist/types/compliance-rules/privacy.d.ts +1 -4
  90. package/dist/types/compliance-rules/shared.d.ts +0 -2
  91. package/dist/types/compliance-rules/strict.d.ts +0 -5
  92. package/dist/types/compliance.d.ts +0 -3
  93. package/dist/types/conditions.d.ts +19 -0
  94. package/dist/types/config.d.ts +21 -2
  95. package/dist/types/context.d.ts +66 -0
  96. package/dist/types/ecosystem-cli.d.ts +17 -0
  97. package/dist/types/egress.d.ts +46 -0
  98. package/dist/types/examples.d.ts +50 -0
  99. package/dist/types/explain-cli.d.ts +11 -0
  100. package/dist/types/explain.d.ts +95 -0
  101. package/dist/types/extensions.d.ts +177 -0
  102. package/dist/types/function-sources.d.ts +9 -0
  103. package/dist/types/functions.d.ts +48 -5
  104. package/dist/types/guest-api.d.ts +1 -0
  105. package/dist/types/index.d.ts +36 -6
  106. package/dist/types/init-with.d.ts +30 -0
  107. package/dist/types/interchange-cli.d.ts +16 -0
  108. package/dist/types/interchange.d.ts +42 -0
  109. package/dist/types/manifest.d.ts +79 -0
  110. package/dist/types/match.d.ts +1 -0
  111. package/dist/types/mcp-authoring.d.ts +92 -0
  112. package/dist/types/mcp.d.ts +12 -0
  113. package/dist/types/observability.d.ts +3 -14
  114. package/dist/types/operator-host.d.ts +8 -0
  115. package/dist/types/plugins.d.ts +2 -0
  116. package/dist/types/policies/agents.d.ts +0 -2
  117. package/dist/types/policies/compression.d.ts +2 -0
  118. package/dist/types/policies/security.d.ts +0 -1
  119. package/dist/types/policy.d.ts +15 -4
  120. package/dist/types/project-tests.d.ts +3 -2
  121. package/dist/types/provider-verification.d.ts +53 -0
  122. package/dist/types/proxy.d.ts +21 -0
  123. package/dist/types/readiness.d.ts +10 -3
  124. package/dist/types/recipes.d.ts +30 -0
  125. package/dist/types/route-diff.d.ts +27 -0
  126. package/dist/types/router.d.ts +2 -1
  127. package/dist/types/runtime.d.ts +11 -27
  128. package/dist/types/sandbox.d.ts +12 -0
  129. package/dist/types/scaffold.d.ts +0 -2
  130. package/dist/types/schema-query.d.ts +12 -0
  131. package/dist/types/server.d.ts +1 -4
  132. package/dist/types/signals.d.ts +25 -0
  133. package/dist/types/site.d.ts +0 -1
  134. package/dist/types/tooling.d.ts +115 -0
  135. package/dist/types/trusted-functions.d.ts +29 -0
  136. package/dist/types/types.d.ts +71 -7
  137. package/dist/types/typescript-authoring.d.ts +12 -0
  138. package/dist/types/vercel.d.ts +3 -1
  139. package/dist/types/verify-deployment.d.ts +47 -0
  140. package/dist/types.js +37 -5
  141. package/dist/typescript-authoring.js +142 -0
  142. package/dist/vercel.js +4 -3
  143. package/dist/verify-deployment.js +270 -0
  144. package/docs/AI-AUTHORING.md +232 -15
  145. package/docs/AWS.md +4 -4
  146. package/docs/BEST-PRACTICES.md +3 -2
  147. package/docs/BULK.md +79 -0
  148. package/docs/CAPABILITIES.md +192 -0
  149. package/docs/CAPACITY.md +129 -32
  150. package/docs/CI.md +142 -0
  151. package/docs/CLOUDFLARE.md +1 -2
  152. package/docs/COMPLIANCE.md +6 -9
  153. package/docs/CONDITIONS.md +74 -0
  154. package/docs/DEPLOYMENT-CHECKS.md +108 -0
  155. package/docs/EGRESS.md +125 -0
  156. package/docs/EXTENSIONS.md +398 -0
  157. package/docs/FRAMEWORK.md +198 -0
  158. package/docs/FUNCTION-SECURITY.md +129 -32
  159. package/docs/INSTALL.md +45 -12
  160. package/docs/INTERCHANGE.md +134 -0
  161. package/docs/LOAD-TESTING.md +4 -4
  162. package/docs/MIDDLEWARE-EXAMPLES.md +75 -0
  163. package/docs/MIDDLEWARE.md +29 -16
  164. package/docs/MONITORING.md +2 -19
  165. package/docs/NEXT-PHASE-PLAN.md +98 -0
  166. package/docs/NEXT-STEPS.md +634 -0
  167. package/docs/OBSERVABILITY.md +11 -18
  168. package/docs/OPEN-DECISIONS.md +212 -0
  169. package/docs/OPERATIONAL-PROOF.md +30 -31
  170. package/docs/OPERATIONS.md +29 -35
  171. package/docs/PLUGINS.md +37 -0
  172. package/docs/POLICIES.md +23 -309
  173. package/docs/PRERENDER.md +41 -1
  174. package/docs/PROJECT-DIRECTION.md +75 -8
  175. package/docs/PROVIDER-VERIFICATION.md +84 -0
  176. package/docs/READINESS.md +21 -1
  177. package/docs/README.md +87 -34
  178. package/docs/RECIPES.md +99 -0
  179. package/docs/RELEASE-READINESS.md +57 -35
  180. package/docs/RELEASE-SECURITY.md +116 -7
  181. package/docs/RESILIENCE.md +16 -15
  182. package/docs/ROUTING.md +8 -10
  183. package/docs/SANDBOX-REVIEW.md +19 -6
  184. package/docs/SCAFFOLDING.md +0 -2
  185. package/docs/SECURITY-AUDIT.md +41 -1
  186. package/docs/SPECIFICATION.md +150 -29
  187. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +287 -0
  188. package/docs/SPIKE-BUSINESS-SUITE.md +1021 -0
  189. package/docs/SPIKE-CORE-LAYERING.md +337 -0
  190. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +209 -0
  191. package/docs/SPIKE-EXTENSION-MODEL.md +419 -0
  192. package/docs/SPIKE-EXTENSIONS.md +6 -0
  193. package/docs/SPIKE-LAMBDA-COMPILE.md +201 -0
  194. package/docs/SPIKE-MONOREPO.md +322 -0
  195. package/docs/STANDARDS.md +150 -142
  196. package/docs/STARTERS.md +21 -1
  197. package/docs/STATIC.md +94 -0
  198. package/docs/TOOLING.md +295 -0
  199. package/docs/TUNNELS.md +0 -3
  200. package/docs/TYPESCRIPT-AUTHORING.md +82 -0
  201. package/docs/TYPESCRIPT.md +25 -4
  202. package/docs/USABILITY-REVIEW.md +129 -0
  203. package/docs/VERCEL.md +4 -5
  204. package/docs/VERSION-ALIGNMENT.md +205 -0
  205. package/docs/YAML-GUIDE.md +15 -479
  206. package/docs/YAML-REFERENCE.md +143 -22
  207. package/docs/policies/agents.md +1 -1
  208. package/docs/policies/cache.md +13 -0
  209. package/docs/policies/contract.md +52 -0
  210. package/docs/policies/hardened.md +56 -0
  211. package/docs/policies/interoperability.md +169 -0
  212. package/docs/policies/operations.md +45 -0
  213. package/docs/yaml/assets.md +36 -0
  214. package/docs/yaml/conditions.md +20 -0
  215. package/docs/yaml/functions.md +160 -0
  216. package/docs/yaml/middleware.md +29 -0
  217. package/docs/yaml/organization.md +74 -0
  218. package/docs/yaml/policies.md +37 -0
  219. package/docs/yaml/redirects.md +64 -0
  220. package/docs/yaml/responses.md +57 -0
  221. package/docs/yaml/site.md +24 -0
  222. package/examples/assets/example.yaml +17 -0
  223. package/examples/aws/example.yaml +20 -0
  224. package/examples/cloudflare/example.yaml +19 -0
  225. package/examples/compliance/example.yaml +11 -0
  226. package/examples/conditions/README.md +12 -0
  227. package/examples/conditions/example.yaml +19 -0
  228. package/examples/conditions/tests/requests.json +13 -0
  229. package/examples/conditions/urlcode.yaml +24 -0
  230. package/examples/cookbook/README.md +8 -4
  231. package/examples/cookbook/example.yaml +17 -0
  232. package/examples/cookbook/functions/catalog.mjs +3 -0
  233. package/examples/cookbook/functions/fail.mjs +4 -0
  234. package/examples/cookbook/functions/items.mjs +3 -0
  235. package/examples/cookbook/functions/profile.mjs +3 -0
  236. package/examples/cookbook/functions/resource.mjs +3 -0
  237. package/examples/cookbook/functions/status.mjs +3 -0
  238. package/examples/cookbook/middleware/auth.mjs +48 -0
  239. package/examples/cookbook/middleware/body.mjs +15 -0
  240. package/examples/cookbook/middleware/bucket.mjs +29 -0
  241. package/examples/cookbook/middleware/cors.mjs +21 -0
  242. package/examples/cookbook/middleware/debug.mjs +13 -0
  243. package/examples/cookbook/middleware/envelope.mjs +11 -0
  244. package/examples/cookbook/middleware/errors.mjs +11 -0
  245. package/examples/cookbook/middleware/etag.mjs +18 -0
  246. package/examples/cookbook/middleware/locale.mjs +20 -0
  247. package/examples/cookbook/middleware/maintenance.mjs +10 -0
  248. package/examples/cookbook/middleware/methods.mjs +15 -0
  249. package/examples/cookbook/middleware/negotiate.mjs +20 -0
  250. package/examples/cookbook/middleware/referer.mjs +12 -0
  251. package/examples/cookbook/middleware/request-id.mjs +16 -0
  252. package/examples/cookbook/route-index.json +676 -0
  253. package/examples/cookbook/routes/middleware.yaml +126 -0
  254. package/examples/cookbook/tests/requests.json +526 -0
  255. package/examples/cookbook/urlcode.yaml +1 -0
  256. package/examples/egress/README.md +22 -0
  257. package/examples/egress/example.yaml +19 -0
  258. package/examples/egress/urlcode.yaml +19 -0
  259. package/examples/extensions/README.md +7 -0
  260. package/examples/extensions/example.yaml +21 -0
  261. package/examples/extensions/urlcode.yaml +25 -0
  262. package/examples/monitoring/example.yaml +8 -0
  263. package/examples/prerender/README.md +2 -2
  264. package/examples/prerender/example.yaml +16 -0
  265. package/examples/provider-conformance/README.md +12 -0
  266. package/examples/provider-conformance/example.yaml +14 -0
  267. package/examples/provider-conformance/urlcode.yaml +34 -0
  268. package/examples/tunnel/example.yaml +8 -0
  269. package/examples/vercel/example.yaml +19 -0
  270. package/llms-full.txt +3084 -0
  271. package/llms.txt +61 -21
  272. package/package.json +36 -7
  273. package/packaging/claude-plugin/.claude-plugin/plugin.json +19 -0
  274. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +122 -0
  275. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +108 -0
  276. package/recipes/authenticated-json-api/README.md +51 -0
  277. package/recipes/authenticated-json-api/functions/profile.mjs +5 -0
  278. package/recipes/authenticated-json-api/recipe.yaml +34 -0
  279. package/recipes/authenticated-json-api/tests/requests.json +39 -0
  280. package/recipes/authenticated-json-api/urlcode.yaml +12 -0
  281. package/recipes/contact-form/README.md +25 -0
  282. package/recipes/contact-form/functions/contact.mjs +17 -0
  283. package/recipes/contact-form/recipe.yaml +33 -0
  284. package/recipes/contact-form/tests/requests.json +47 -0
  285. package/recipes/contact-form/urlcode.yaml +18 -0
  286. package/recipes/cors-api/README.md +16 -0
  287. package/recipes/cors-api/functions/items.mjs +3 -0
  288. package/recipes/cors-api/middleware/cors.mjs +21 -0
  289. package/recipes/cors-api/recipe.yaml +26 -0
  290. package/recipes/cors-api/tests/requests.json +65 -0
  291. package/recipes/cors-api/urlcode.yaml +12 -0
  292. package/recipes/health-page/README.md +13 -0
  293. package/recipes/health-page/recipe.yaml +23 -0
  294. package/recipes/health-page/tests/requests.json +36 -0
  295. package/recipes/health-page/urlcode.yaml +19 -0
  296. package/recipes/json-api/README.md +6 -0
  297. package/recipes/json-api/functions/echo.mjs +3 -0
  298. package/recipes/json-api/recipe.yaml +25 -0
  299. package/recipes/json-api/tests/requests.json +34 -0
  300. package/recipes/json-api/urlcode.yaml +12 -0
  301. package/recipes/middleware/README.md +34 -0
  302. package/recipes/middleware/functions/catalog.mjs +3 -0
  303. package/recipes/middleware/functions/fail.mjs +4 -0
  304. package/recipes/middleware/functions/items.mjs +3 -0
  305. package/recipes/middleware/functions/profile.mjs +3 -0
  306. package/recipes/middleware/functions/resource.mjs +3 -0
  307. package/recipes/middleware/functions/status.mjs +3 -0
  308. package/recipes/middleware/middleware/auth.mjs +48 -0
  309. package/recipes/middleware/middleware/body.mjs +15 -0
  310. package/recipes/middleware/middleware/bucket.mjs +29 -0
  311. package/recipes/middleware/middleware/cors.mjs +21 -0
  312. package/recipes/middleware/middleware/debug.mjs +13 -0
  313. package/recipes/middleware/middleware/envelope.mjs +11 -0
  314. package/recipes/middleware/middleware/errors.mjs +11 -0
  315. package/recipes/middleware/middleware/etag.mjs +18 -0
  316. package/recipes/middleware/middleware/locale.mjs +20 -0
  317. package/recipes/middleware/middleware/maintenance.mjs +10 -0
  318. package/recipes/middleware/middleware/methods.mjs +15 -0
  319. package/recipes/middleware/middleware/negotiate.mjs +20 -0
  320. package/recipes/middleware/middleware/referer.mjs +12 -0
  321. package/recipes/middleware/middleware/request-id.mjs +16 -0
  322. package/recipes/middleware/public/guide.txt +1 -0
  323. package/recipes/middleware/recipe.yaml +50 -0
  324. package/recipes/middleware/tests/requests.json +528 -0
  325. package/recipes/middleware/urlcode.yaml +127 -0
  326. package/recipes/protected-download/README.md +22 -0
  327. package/recipes/protected-download/files/report.txt +1 -0
  328. package/recipes/protected-download/recipe.yaml +31 -0
  329. package/recipes/protected-download/tests/requests.json +32 -0
  330. package/recipes/protected-download/urlcode.yaml +15 -0
  331. package/recipes/redirect/README.md +7 -0
  332. package/recipes/redirect/recipe.yaml +25 -0
  333. package/recipes/redirect/tests/requests.json +19 -0
  334. package/recipes/redirect/urlcode.yaml +9 -0
  335. package/recipes/static-plus-api/README.md +15 -0
  336. package/recipes/static-plus-api/functions/info.mjs +3 -0
  337. package/recipes/static-plus-api/public/assets/index.html +3 -0
  338. package/recipes/static-plus-api/public/assets/site.css +1 -0
  339. package/recipes/static-plus-api/public/index.html +8 -0
  340. package/recipes/static-plus-api/recipe.yaml +29 -0
  341. package/recipes/static-plus-api/tests/requests.json +56 -0
  342. package/recipes/static-plus-api/urlcode.yaml +23 -0
  343. package/recipes/typescript/README.md +8 -0
  344. package/recipes/typescript/functions/hello.ts +5 -0
  345. package/recipes/typescript/recipe.yaml +23 -0
  346. package/recipes/typescript/tests/requests.json +18 -0
  347. package/recipes/typescript/urlcode.yaml +5 -0
  348. package/recipes/webhook-receiver/README.md +20 -0
  349. package/recipes/webhook-receiver/functions/receive.mjs +16 -0
  350. package/recipes/webhook-receiver/recipe.yaml +27 -0
  351. package/recipes/webhook-receiver/tests/requests.json +59 -0
  352. package/recipes/webhook-receiver/urlcode.yaml +23 -0
  353. package/schemas/recipe.schema.json +139 -0
  354. package/schemas/urlcode.schema.json +659 -110
  355. package/skills/urlcode/SKILL.md +119 -0
  356. package/starters/default/.github/workflows/urlcode.yml +23 -0
  357. package/starters/default/.mcp.json +12 -0
  358. package/starters/default/AGENTS.md +79 -0
  359. package/starters/default/urlcode.yaml +0 -1
  360. package/dist/link-api.js +0 -136
  361. package/dist/link-cli.js +0 -141
  362. package/dist/link-events.js +0 -76
  363. package/dist/link-records.js +0 -31
  364. package/dist/link-store-worker.js +0 -150
  365. package/dist/link-store.js +0 -250
  366. package/dist/management-policy.js +0 -41
  367. package/dist/sqlite-version.js +0 -6
  368. package/dist/types/link-api.d.ts +0 -30
  369. package/dist/types/link-cli.d.ts +0 -36
  370. package/dist/types/link-events.d.ts +0 -27
  371. package/dist/types/link-records.d.ts +0 -11
  372. package/dist/types/link-store-worker.d.ts +0 -1
  373. package/dist/types/link-store.d.ts +0 -130
  374. package/dist/types/management-policy.d.ts +0 -9
  375. package/dist/types/sqlite-version.d.ts +0 -1
  376. package/docs/DYNAMIC-LINKS.md +0 -561
  377. package/docs/MANAGEMENT-SECURITY.md +0 -82
  378. package/examples/live-links/README.md +0 -11
  379. package/examples/live-links/tests/requests.json +0 -6
  380. package/examples/live-links/urlcode.yaml +0 -16
package/docs/BULK.md ADDED
@@ -0,0 +1,79 @@
1
+ # Bulk redirect projects
2
+
3
+ Bulk import converts CSV, JSON or YAML redirect rows into an ordinary Git-owned
4
+ URLCode project. It delegates semantics and row diagnostics to the strict
5
+ [redirect interchange](INTERCHANGE.md) converter. Only literal paths and
6
+ absolute HTTP(S) redirect destinations are accepted; unsupported behavior and
7
+ duplicate paths fail rather than being discarded or overwritten.
8
+
9
+ ```sh
10
+ urlcode bulk-import csv redirects.csv --out ./imported --dry-run
11
+ urlcode bulk-import csv redirects.csv --out ./imported
12
+ urlcode validate --local --project ./imported
13
+ ```
14
+
15
+ CSV requires the header `path,url,status`; status can be empty for 302. JSON and
16
+ YAML are arrays of `{path, url, status?}` rows. Input is limited to 32 MiB and
17
+ 100,000 routes. This command is for data import, not arbitrary provider config.
18
+ Use explicit provider interchange commands when migrating provider files.
19
+
20
+ The SDK provides
21
+ `importBulkProject(text, format, output, {dryRun, source})`. `format` is `csv`,
22
+ `json` or `yaml`. The report includes `ok`, `routeCount`, source/row diagnostics,
23
+ a SHA-256 fingerprint of the exact input text, and a bounded output-file plan.
24
+ Invalid input returns `ok: false` without writing files. Filesystem/output
25
+ refusals throw. `source` is a caller-supplied provenance label, not a file to read.
26
+
27
+ Routes are sorted by literal path and divided into include files with at most
28
+ 1,000 routes each. At 100,000 routes this produces 100 include files. The entry
29
+ `urlcode.yaml` holds the include list and empty `routes`. `provenance.json`
30
+ records the source label, input fingerprint, counts and each shard's first/last
31
+ path. It does not embed the original input or claim exact source line mappings
32
+ for successful output rows; retain the original file in version control if that
33
+ traceability is needed. Invalid row diagnostics preserve their source positions.
34
+
35
+ Dry-run performs conversion and output planning without writing the requested
36
+ destination. Import requires a new output directory under an existing parent;
37
+ there is no implicit merge or overwrite mode. As with recipes, dependencies are
38
+ written first and a rename publishes the completed entry YAML last. Failed
39
+ writes clean up the directory created by that invocation. Source data is never
40
+ interpreted as shell code. The caller must control the output parent while
41
+ publishing.
42
+
43
+ ## Local scale evidence
44
+
45
+ Run each dataset in a fresh process, sequentially, without concurrent builds:
46
+
47
+ ```sh
48
+ node benchmarks/bulk.ts 1000
49
+ node benchmarks/bulk.ts 10000
50
+ node benchmarks/bulk.ts 100000
51
+ ```
52
+
53
+ Observed on 2026-09-17 against the final integrated next-phase working tree,
54
+ with the merged TypeScript 6.0.3 dependency lock, Node v26.8.2, macOS arm64,
55
+ Apple M4 Pro and 48 GiB system memory. These measurements include the final
56
+ capability, revision-pin and egress activation paths. Synthetic routes redirect `/rN` to `https://example.com/items/N`.
57
+ Conversion includes input validation, semantic compilation and sharded output
58
+ publication; activation uses the normal `createRuntime` loader and compiler.
59
+ Lookup measures 5,000 `Runtime.handle` requests at concurrency one after 100
60
+ warm-up requests, validating each status and Location. It excludes socket/TLS
61
+ transport. Memory is sampled process RSS/heap after phases, not peak memory or
62
+ an isolated worker measurement.
63
+
64
+ | Routes | Includes | Conversion ms | Activation ms | RSS after activation MiB | Heap MiB | Lookup p95 ms |
65
+ | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
66
+ | 1,000 | 1 | 56 | 139 | 180 | 27 | 0.002292 |
67
+ | 10,000 | 10 | 272 | 373 | 288 | 46 | 0.002916 |
68
+ | 100,000 | 100 | 5,530 | 8,838 | 537 | 214 | 0.003791 |
69
+
70
+ All three sequential fresh-process datasets activated and returned the expected
71
+ redirects. These are single observations, not a repeated-run statistical study;
72
+ background host load can change startup and memory measurements. Unlike the
73
+ previous single-document 100,000-route routing benchmark, the sharded project
74
+ fits the existing configuration worker's 256 MiB heap and 10-second deadline.
75
+ No loader, route-count, compiler deadline or runtime safety limit was increased.
76
+ This local result does not establish cross-platform capacity, provider
77
+ performance, peak-memory bounds, concurrency/soak behavior or production SLOs.
78
+ The benchmark reports failure phase and error if a limit is reached on another
79
+ machine; it does not retry with relaxed limits.
@@ -0,0 +1,192 @@
1
+ # Capabilities and normalized route representation
2
+
3
+ URLCode is a portable runtime for programmable URL behavior: **URL behavior as
4
+ code**. YAML describes behavior; operators supply infrastructure and authority.
5
+
6
+ ## Inspect target support
7
+
8
+ ```sh
9
+ urlcode capabilities
10
+ urlcode capabilities --target self-hosted
11
+ urlcode capabilities --target cloudflare --json
12
+ urlcode capabilities --target aws
13
+ urlcode capabilities --target vercel
14
+ urlcode capabilities --target static
15
+ ```
16
+
17
+ This command needs no project or credentials. `node` is an alias for
18
+ `self-hosted`, matching the existing embedding API. Unknown target names fail.
19
+ JSON has `format: 1`, target deployment evidence and capability rows.
20
+ `doctor` also reports `capabilityTargets`; its `providers` list remains empty
21
+ because no provider deployment has been verified. Canonical
22
+ names follow the schema (`respond`, `extension`, `policies.security`), not
23
+ marketing synonyms. `proxy` and `signals` are self-hosted capabilities requiring
24
+ external revision-pinned origin grants. `conditions` (`match`) and `conditional`
25
+ (disjoint cases) are supported by self-hosted/AWS/Vercel and refused by
26
+ Cloudflare (no artifact lowering yet) and by `static` (no server to match a
27
+ request against). `extension`/`policies.extensions` report per-extension
28
+ support from the registered extension's own declared `targets` when a
29
+ `--host-file` is supplied; without one they report `conditional`/`unknown`
30
+ rather than a blanket answer. See [egress](EGRESS.md) and
31
+ [conditions](CONDITIONS.md).
32
+
33
+ | Support | Meaning |
34
+ | --- | --- |
35
+ | native | Implemented by the local runtime or Node adapter |
36
+ | compiled | Implemented by the Cloudflare or static-hosting compiler and its runtime/build output |
37
+ | conditional | Depends on configuration; inspect the actual project |
38
+ | delegated | Existing policy contract relies on provider behavior |
39
+ | refused | No implementation that this target can activate |
40
+ | unknown | No support evidence; fail closed during project analysis |
41
+
42
+ `native` and `compiled` describe local implementation tests. AWS, Vercel,
43
+ Cloudflare and static deployment evidence remains **unverified**. This is not a
44
+ blanket exact-portability promise. Cloudflare coalesces duplicate headers and
45
+ receives a normalized Request target; AWS accepts payload v2 only; `static` has
46
+ no server at all, so it refuses every capability that needs one (parameters,
47
+ request bodies, response headers, bindings, every `policies.*`) in addition to
48
+ `function`/`middleware`. See [Cloudflare](CLOUDFLARE.md), [AWS](AWS.md),
49
+ [Vercel](VERCEL.md) and [static hosting](STATIC.md) for transport and fidelity
50
+ limits. Compression is explicitly delegated, not verified equivalent to
51
+ operator-selected settings. Route throttle counters and caches remain per
52
+ instance. No supported entry bypasses semantic validation, required operator
53
+ grants or deployment prerequisites.
54
+
55
+ ## One capability or one schema fragment
56
+
57
+ ```sh
58
+ urlcode capabilities redirect
59
+ urlcode capabilities policies.throttle --json
60
+ urlcode schema route
61
+ urlcode schema policies.cache --json
62
+ urlcode schema site.sitemap --yaml
63
+ ```
64
+
65
+ `urlcode capabilities <name>` prints one catalog entry: its kind (handler,
66
+ policy, routing, request, binding, egress, middleware or project), a summary,
67
+ the resolved schema fragment(s), constraints, the operator grants the capability
68
+ needs at activation, support per target, the targets that refuse it, and the
69
+ bundled recipes and cookbook routes that use it. Names are the catalog names
70
+ (`redirect`, `bindings`, `policies.cache`); an unknown name fails with exit 1
71
+ and lists the valid names. `--target` applies to the full catalog only.
72
+
73
+ `urlcode schema <path>` prints only that fragment of
74
+ `schemas/urlcode.schema.json` with local `$ref`s resolved inline. Paths are
75
+ top-level document keys (`routes`, `policies`, `site`, `extensions`), `route`,
76
+ or a route property (`redirect`, `middleware`, `match`, `env`), optionally
77
+ followed by nested property names (`policies.cache`, `request.body`,
78
+ `site.sitemap`). Resolution is bounded and cycle-safe; where a nested object is
79
+ its own path (`route` inside `routes`, `policies` inside `route`) it is
80
+ summarized with a `$comment` naming that path so every fragment stays under
81
+ 16 KiB. Fragments describe shape only: they carry no defaults, validation
82
+ result or operator authority, and the full schema remains the contract.
83
+
84
+ Both commands read bundled package data and need no project, credentials or
85
+ network. The SDK exposes them as `getCapability(name)` and
86
+ `getSchemaFragment(path)`; the MCP server as `get_capability` and `get_schema`
87
+ (see [tooling](TOOLING.md)). Grant descriptions name the operator flag or policy
88
+ involved, never binding values.
89
+
90
+ ## Programmatic analysis
91
+
92
+ The main package exports `getCapabilities`, `routeCapabilities`,
93
+ `analyzeProjectCapabilities`, `analyzeCompiledCapabilities`,
94
+ `assertTargetCompatibility`, `normalizeCapabilityTarget` and their report types.
95
+
96
+ ```js
97
+ import { loadDocument, analyzeProjectCapabilities,
98
+ assertTargetCompatibility } from '@jimhoyd/urlcode';
99
+
100
+ const loaded = await loadDocument('./project');
101
+ const report = analyzeProjectCapabilities(loaded, 'cloudflare');
102
+ console.log(report.issues); // path, capability, support, reason; never binding values
103
+ assertTargetCompatibility(report);
104
+ ```
105
+
106
+ This low-level example examines declared routes. Runtime activation and builds
107
+ first expand `site` conventions using the operator origin, then analyze all
108
+ routes including generated ones. A report is a compatibility preflight, **not**
109
+ a substitute for compilation/validation. Disabled and expired routes are still
110
+ checked. `analyzeProjectCapabilities`/`analyzeCompiledCapabilities` take an
111
+ optional resolved extension registration set (the same shape `--host-file`
112
+ loads); pass it to get per-extension `refused`/`native` from that extension's
113
+ own `targets` instead of the generic `conditional`/`unknown` answer.
114
+ `compatible` means there are no refused, unknown or unresolved conditional
115
+ requirements. Explicit delegation and transport limitations still apply.
116
+
117
+ Requirements include effective inherited/profile policies after route overrides
118
+ and `false` removals. Policy modules' existing `targets(config)` functions remain
119
+ the source of policy decisions. The catalog says serverless throttle is
120
+ conditional; a project report resolves `partition: route` to native and the
121
+ client partitions to refused. Reports contain paths and capability facts, not
122
+ sources, destinations, binding names/values, code, validator closures or assets.
123
+
124
+ Build/activation refusals aggregate all incompatible requirements and name each
125
+ route, capability, target and reason before any artifact files are written.
126
+ Unsupported bindings on Cloudflare fail before credentials are resolved.
127
+
128
+ ## Existing IR, formalized
129
+
130
+ The implementation already has a useful internal representation:
131
+
132
+ ```text
133
+ strict YAML + schema validation + includes
134
+ → site expansion
135
+ → shared capability preflight (declarations, no credentials)
136
+ → semantic route compilation
137
+ → CompiledRouteTable / CompiledRoute
138
+ → capability analysis / target lowering
139
+ → host assets + policy chains + trusted or isolated function dispatch, or Worker artifact
140
+ ```
141
+
142
+ `CompiledRoute` in `src/types.ts` extends shared `MatchableRoute` with validated
143
+ parameters, normalized HTTP replies/headers, resolved bindings and source
144
+ references. `CompiledRouteTable` indexes literal paths, parameter buckets and
145
+ static mounts. `router.ts` owns precedence, collision checks, default methods,
146
+ input/reference validation and normalization. `match.ts` supplies portable
147
+ request-time matching to both runtime and Worker. No second parser or competing
148
+ route IR is introduced.
149
+
150
+ `routeCapabilities` is a value-free projection used for both declaration
151
+ preflight and compiled analysis. Preflight deliberately precedes full semantic
152
+ validation to report unsupported features before reading missing assets or
153
+ resolving secrets; it cannot authorize or validate a project. Normal compilation
154
+ and policy validation still run. AWS/Vercel consume the shared runtime IR;
155
+ Cloudflare additionally analyzes the compiled table before serializing its
156
+ allowlisted artifact fields and standalone validators.
157
+
158
+ The compiled table is internal, mutable during activation and **not serializable
159
+ as an interchange contract**: bindings contain resolved secrets, validators are
160
+ functions, assets contain bytes and policy chains own host state. The existing
161
+ Cloudflare artifact is a separate versioned lowering, not a replacement IR.
162
+ [Interchange](INTERCHANGE.md) projects a validated literal-redirect subset and
163
+ explicitly reports unsupported semantics; it never dumps compiled routes.
164
+ The next-phase schema extends this same IR with normalized condition cases,
165
+ proxy headers and signal definitions. Resolved egress headers are private runtime
166
+ state and must never be serialized. Capability analysis itself adds no authority
167
+ and does not run in the request path.
168
+
169
+ See the [repository review and incremental plan](NEXT-PHASE-PLAN.md) for the
170
+ implementation status. Provider deployment tests, independent security review and
171
+ operational soak/recovery proof are separate work.
172
+
173
+ ## Local performance check
174
+
175
+ 2026-09-17, Node 26.8.2, macOS arm64, Apple M4 Pro, 48 GiB RAM.
176
+ `npm run benchmark -- <count>` runs 5,000 loopback requests at concurrency 16.
177
+ Single runs against baseline `1a00294` and this change, not a statistical study:
178
+
179
+ | Routes | Startup ms before / after | RSS MiB before / after | Heap MiB before / after | Requests/s before / after | p95 ms before / after |
180
+ | --- | --- | --- | --- | --- | --- |
181
+ | 1,000 | 135 / 138 | 183 / 182 | 29 / 34 | 24,645 / 24,701 | 1.18 / 1.23 |
182
+ | 10,000 | 312 / 303 | 211 / 207 | 55 / 63 | 6,762 / 6,829 | 4.08 / 4.09 |
183
+
184
+ Both baseline and updated 100,000-route runs failed with `Configuration worker
185
+ resource limit or failure` before route compilation. The worker's existing
186
+ resource bounds are unchanged; the configured 100k route ceiling is not evidence
187
+ that every 100k YAML document fits those bounds. Bulk-scale remediation and
188
+ repeatable memory profiling were subsequent work at Phase A. The new
189
+ [bulk sharding benchmark](BULK.md) successfully activates 100,000 routes within
190
+ the unchanged worker limits. Capability analysis adds
191
+ linear activation work and temporary report allocations; no request-time checks
192
+ were added. These measurements are not provider, soak or capacity certification.
package/docs/CAPACITY.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Capacity, concurrency and system limits
2
2
 
3
- These are 0.2.0 implementation limits and planning models, not a throughput
3
+ These are 0.3.0 implementation limits and planning models, not a throughput
4
4
  SLA. Route count, connections, in-flight requests and sandbox concurrency are
5
5
  four different quantities. Always measure the actual application on deployment
6
6
  hardware with the intended proxy, TLS, logging and limits enabled.
@@ -13,16 +13,56 @@ lookup after path parsing). Parameter candidates are grouped by segment count
13
13
  and scanned in specificity order; matching is O(P × L) in the worst case for P
14
14
  candidates and L segments. Static mount prefixes are scanned longest first.
15
15
 
16
- Plain redirects, declared responses, stored-link lookups and assets do not enter
17
- the sandbox. Stored links use a separate bounded database pools. A
18
- function or any attached middleware occupies one shared worker slot for its
19
- whole chain. Workers are shared by all programmable routes in that snapshot;
20
- there is no per-route fairness or reserved capacity. Awaiting guest timers still
21
- occupies the slot. A fresh guest and module initialization are part of each call.
16
+ Plain redirects, declared responses and assets do not enter
17
+ the sandbox or the trusted executor.
18
+
19
+ `function`/`middleware` routes have **two distinct capacity models**, chosen
20
+ per route by `sandbox` (docs/SPIKE-DEFAULT-TRUST-MODEL.md):
21
+
22
+ - **`sandbox: true` (the isolated worker pool, unchanged from every earlier
23
+ release):** a function or any attached middleware occupies one shared
24
+ worker slot for its whole chain. Workers are shared by all `sandbox: true`
25
+ routes in that snapshot; there is no per-route fairness or reserved
26
+ capacity. Awaiting guest timers still occupies the slot. A fresh guest and
27
+ module initialization are part of each call. See "Enforced limits and
28
+ defaults" below for the numbers (2 workers, 5 s deadline, 32 MiB heap).
29
+ - **`sandbox` false/absent (the trusted default):** the call runs in-process,
30
+ on the same event loop as everything else the server does — ordinary Node
31
+ concurrency, not a fixed worker-slot ceiling. There is no separate pool to
32
+ exhaust and no per-invocation heap/module reset: it is bounded by the same
33
+ `--max-in-flight` HTTP admission cap (default 64) that bounds every other
34
+ request, not by a `workers` count. A trusted call's declared `timeoutMs`
35
+ races the call's own promise rather than forcibly terminating a worker
36
+ thread — see "Trusted-path deadlines" below for what that does and does not
37
+ protect against.
38
+
39
+ This was architectural reasoning, not a measurement, until the run below:
40
+ see "Measured: sandboxed vs trusted dispatch" for the actual comparison.
22
41
 
23
42
  Node's main event loop remains a shared bottleneck for HTTP parsing, logging and
24
- native responses. Sandboxing contains application code authority and bounds
25
- individual execution; it does not make all host resources immune to exhaustion.
43
+ native responses. The sandbox contains a `sandbox: true` route's application
44
+ code authority and bounds its individual execution; the trusted default does
45
+ not attempt to, by design. Neither mode makes all host resources immune to
46
+ exhaustion.
47
+
48
+ ### Trusted-path deadlines
49
+
50
+ A sandboxed worker's deadline is enforced by an interrupt handler the WASM
51
+ engine checks between guest operations, backed by an independent outer
52
+ termination that kills the worker thread if the guest never yields — the
53
+ worker (and its slot) can be forcibly reclaimed even from a stuck call. A
54
+ trusted, in-process call has no such mechanism available: `timeoutMs` starts
55
+ a race between the call's promise and a timer, so a call that never resolves
56
+ (an unresolved promise, an awaited operation that never completes) is
57
+ answered with a 504 on schedule, but a call that blocks the event loop
58
+ *synchronously* (an infinite `while` loop, a huge synchronous computation)
59
+ is not preempted — it keeps running, delays that timer's own firing, and
60
+ holds up every other request on the same process until it returns control to
61
+ the event loop or the process is restarted. This is a real, documented
62
+ difference from the sandboxed path's guarantee, not an oversight: Node has no
63
+ supported way to interrupt another turn of the same thread's event loop from
64
+ inside it. A route whose trusted code cannot be trusted to yield promptly is
65
+ exactly the kind of route `sandbox: true` exists for.
26
66
 
27
67
  ## Enforced limits and defaults
28
68
 
@@ -39,15 +79,17 @@ individual execution; it does not make all host resources immune to exhaustion.
39
79
  | Requests per socket | 1,000 | Connection recycling; not a requests-per-second limit |
40
80
  | Header / request receipt / keep-alive timeouts | 10 s / 15 s / 5 s | These are not an overall end-to-end response deadline |
41
81
  | Request body | 1 MiB default | Buffered; route maxBytes can tighten to 0–1 MiB |
42
- | Sandbox concurrency | 2 workers, no queue | Shared per snapshot; full pool returns 503 |
43
- | Execution deadline | 5 s default | Entire middleware + handler invocation; timeout returns 504 |
44
- | Guest heap / stack | 32 MiB / 512 KiB | Fresh per invocation; not a bound on total process RSS |
45
- | Outer worker old-generation V8 budget | 128 MiB | Separate from WASM/host/native allocations |
46
- | Function response | 1 MiB default, 16 KiB / 256 header pairs | Buffered text/JSON; YAML headers also bounded |
47
- | Middleware | 16 entries per route | One shared slot/deadline, not 16 independent workers |
48
- | Function sources | 128 modules, 1 MiB/module, 4 MiB total | Project snapshot, including middleware dependencies |
49
- | Worker startup | 5 s deadline | Failure rejects activation; no untrusted host fallback |
50
- | Worker replacement | Up to 3 exits/minute per slot trigger replacement | Further churn leaves the slot unavailable until reload/restart |
82
+ | Sandbox concurrency (`sandbox: true` only) | 2 workers, no queue | Shared per `sandbox: true` snapshot; full pool returns 503 |
83
+ | Sandbox execution deadline (`sandbox: true` only) | 5 s default | Entire middleware + handler invocation; forcibly terminates the worker; timeout returns 504 |
84
+ | Trusted concurrency (`sandbox` false/absent, the default) | Ordinary Node concurrency | Bounded by `--max-in-flight` (default 64), not a worker count; no separate pool to exhaust |
85
+ | Trusted execution deadline (`sandbox` false/absent) | 5 s default (same `timeoutMs` knob) | Races the call's promise; cannot preempt synchronous event-loop-blocking code (see "Trusted-path deadlines" above); timeout returns 504 |
86
+ | Guest heap / stack (`sandbox: true` only) | 32 MiB / 512 KiB | Fresh per invocation; not a bound on total process RSS |
87
+ | Outer worker old-generation V8 budget (`sandbox: true` only) | 128 MiB | Separate from WASM/host/native allocations |
88
+ | Function response | 1 MiB default, 16 KiB / 256 header pairs | Buffered text/JSON; YAML headers also bounded; applies to both execution modes |
89
+ | Middleware | 16 entries per route | One shared slot/deadline (`sandbox: true`) or one in-process call (trusted), not 16 independent workers either way |
90
+ | Function sources (`sandbox: true` only) | 128 modules, 1 MiB/module, 4 MiB total | Sandboxed snapshot, including middleware dependencies; a trusted route's own source is hashed for grant pinning but not bundled or budget-limited this way (see docs/FUNCTION-SECURITY.md) |
91
+ | Worker startup (`sandbox: true` only) | 5 s deadline | Failure rejects activation; no untrusted host fallback |
92
+ | Worker replacement (`sandbox: true` only) | Up to 3 exits/minute per slot trigger replacement | Further churn leaves the slot unavailable until reload/restart |
51
93
  | Assets | 16 MiB/file, 64 MiB unique contents | Buffered immutable snapshots; 10,000 static entries, depth 20 |
52
94
  | Logger buffering | Drop at 1 MiB stdout buffering | Reports logs_dropped when output recovers |
53
95
 
@@ -71,8 +113,70 @@ increasing a timeout also increases how long an attacker can occupy capacity.
71
113
  The CLI uses defaults. Keep settings identical across replicas unless testing a
72
114
  controlled rollout. See [operations](OPERATIONS.md).
73
115
 
116
+ ## Measured: sandboxed vs trusted dispatch
117
+
118
+ The claim above — that the trusted path has "no fixed worker-slot ceiling"
119
+ and scales as ordinary Node concurrency instead — was written as
120
+ architectural reasoning when the trusted-by-default execution model shipped
121
+ (docs/SPIKE-DEFAULT-TRUST-MODEL.md), not backed by a benchmark run. This is
122
+ that run: `benchmarks/sandbox-vs-trusted.ts`
123
+ (`npm run benchmark:sandbox-vs-trusted`), same 20 ms handler source on both a
124
+ `sandbox: true` route and a trusted route in the same server, default
125
+ settings (`workers: 2`, `maxInFlightRequests: 64`, `timeoutMs: 5000`), 2,000
126
+ requests per concurrency level after a 20-request warmup, keep-alive
127
+ connections. Raw output:
128
+ [benchmarks/results/2026-09-19-sandbox-vs-trusted.json](../benchmarks/results/2026-09-19-sandbox-vs-trusted.json).
129
+
130
+ One development machine: Intel Xeon @ 2.10 GHz, 4 vCPUs, 16 GiB RAM, Linux
131
+ x64, Node v22.22.2, 2026-09-19. This is one machine's numbers, not a
132
+ universal claim — re-run on deployment hardware before sizing anything.
133
+
134
+ | Concurrency | Sandboxed req/s (successful) | Sandboxed shed | Sandboxed p95 | Trusted req/s (successful) | Trusted shed | Trusted p95 |
135
+ |---:|---:|---:|---:|---:|---:|---:|
136
+ | 1 | 39 | 0 / 2000 | 26.9 ms | 47 | 0 / 2000 | 21.5 ms |
137
+ | 2 | 80 | 0 / 2000 | 26.5 ms | 95 | 0 / 2000 | 21.7 ms |
138
+ | 8 | 80 | 1,912 / 2000 | 30.2 ms | 379 | 0 / 2000 | 22.1 ms |
139
+ | 32 | 80 | 1,936 / 2000 | 43.5 ms | 1,465 | 0 / 2000 | 23.5 ms |
140
+ | 128 | 80 | 1,950 / 2000 | 70.0 ms | 2,615 | 521 / 2000 | 80.6 ms |
141
+
142
+ "Sandboxed req/s" and "shed" are separate columns for a reason: throughput
143
+ computed over successful responses only is flat at ~80 req/s from
144
+ concurrency 2 upward because the 2-worker ceiling admits at most two
145
+ in-flight calls at a time and returns 503 immediately for the rest, exactly
146
+ as "Enforced limits and defaults" describes (no queue). The ceiling is
147
+ visible starting at concurrency 8 in this run (already >95% shed) and only
148
+ gets worse as concurrency rises — 2 concurrent in-flight sandboxed calls is
149
+ the practical limit regardless of how much load is offered.
150
+
151
+ The trusted path has no such wall: it scales with offered concurrency —
152
+ 379 req/s at 8, 1,465 req/s at 32, both with zero shedding — until offered
153
+ concurrency (128) exceeds the `--max-in-flight` admission cap (64), at which
154
+ point it starts shedding too (521/2000, p95 rises to 80.6 ms) but keeps
155
+ substantially higher successful throughput (2,615 req/s) than the sandboxed
156
+ path ever reaches at any concurrency tested. This confirms the architectural
157
+ prediction rather than contradicting it: the trusted path's ceiling is the
158
+ admission cap, not a fixed worker count, and it sits far above the sandboxed
159
+ path's 2-worker ceiling in this run. It is still a real ceiling — raising
160
+ `--max-in-flight` moves it, it does not remove it — and it is shared with
161
+ every other route on the process, per "Native routes and mixed traffic"
162
+ below.
163
+
164
+ Caveats specific to this run: one handler shape (a 20 ms guest/host timer,
165
+ chosen to make both ceilings visible rather than to represent any particular
166
+ application), one machine, one process with both route types present
167
+ (so CPU contention between them is part of the trusted numbers, not
168
+ excluded), and no TLS, proxy or production logging in the path. Re-run with
169
+ your own handler's actual latency profile before sizing a deployment; see
170
+ "Establish a deployment budget" below.
171
+
74
172
  ## A useful theoretical model
75
173
 
174
+ This worker-slot model describes the `sandbox: true` path only. A trusted
175
+ route has no fixed worker count to plug in as W; its ceiling is ordinary Node
176
+ request concurrency bounded by `--max-in-flight`, not this model — see
177
+ "Measured: sandboxed vs trusted dispatch" above for what that ceiling looks
178
+ like in practice.
179
+
76
180
  Let W be worker slots, S the measured mean slot occupancy in seconds (including
77
181
  sandbox startup and cleanup effects), and lambda the offered programmable
78
182
  requests per second. An idealized worker ceiling is:
@@ -159,6 +263,13 @@ loop even though the HTTP listener is not restarted. Do not equate atomic swap
159
263
  with zero latency impact or incremental route updates. Prefer candidate replicas
160
264
  and traffic switching for production. `serve` does not watch configuration.
161
265
 
266
+ A trusted route's own entry file is re-imported fresh on every reload (see
267
+ [docs/FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)), matching the sandboxed
268
+ pool rebuilding its whole snapshot; a file that entry only imports is not,
269
+ since ordinary Node module resolution — not a per-reload snapshot — governs
270
+ it. Restart the process rather than reload after editing a trusted route's
271
+ dependency, not just its declared `source`.
272
+
162
273
  Before parser-worker limits were introduced, recorded 100k-route startup RSS was about 621 MiB on one development machine,
163
274
  above the illustrative 512 MiB container example. Route limits are acceptance
164
275
  caps, not a promise that the maximum fits your deployment. See [measurements](PERFORMANCE.md).
@@ -186,20 +297,6 @@ production exercise. The readiness endpoint can stay 200 while all worker slots
186
297
  are busy. Use error/latency signals too. No universal safe RPS can be derived
187
298
  from the route count or these defaults alone. See [resilience](RESILIENCE.md).
188
299
 
189
- ## Optional stored-link capacity
190
-
191
- Each SQLite store defaults to two read-only worker connections; writable stores
192
- add one writer. Independent read/write admission caps default to 32 each across
193
- their pool. Readers are configurable from 1–8, and caps from 1–32. Operations have
194
- a 5-second deadline and one-second SQLite lock wait. Public serving has no writer. The
195
- initial cap is 100,000 stored records across collections; this is separate from
196
- the YAML route count. No lookup cache is used, so visibility does not depend on
197
- cache invalidation. Store failures/overload return 503 and failed workers need
198
- reload/restart; healthy readers can continue while readiness is degraded. Management has a separate listener with 64 connections, 8 KiB
199
- headers and 16 KiB JSON bodies. Rate limiting remains an ingress responsibility.
200
- Do not extrapolate in-memory redirect benchmark numbers to database lookups;
201
- measure disk, writes, contention and restoration on the target host.
202
-
203
300
  Configuration parsing/schema validation now run in a terminated-on-deadline worker;
204
301
  route compilation still runs cooperatively on the host (10 seconds, yields every
205
302
  64 routes). Source, AST, structured-clone output, compiled routes, assets, module
package/docs/CI.md ADDED
@@ -0,0 +1,142 @@
1
+ # Checking a project on GitHub
2
+
3
+ `jimhoyd-com/urlcode/action` is a composite GitHub Action for a URLCode
4
+ *project*: a repository with a `urlcode.yaml`. It runs the same local checks
5
+ you run by hand and, on pull requests, keeps one comment up to date with the
6
+ route-inventory diff against the base branch. It needs no cloud credentials;
7
+ the only token it touches is the workflow's own `GITHUB_TOKEN`.
8
+
9
+ The starter ships it as `.github/workflows/urlcode.yml` (`urlcode init` copies
10
+ it; the [template repository](https://github.com/jimhoyd-com/urlcode-template)
11
+ carries the same file):
12
+
13
+ ```yaml
14
+ name: urlcode
15
+ on:
16
+ push:
17
+ pull_request:
18
+ permissions:
19
+ contents: read
20
+ pull-requests: write # the sticky route-diff comment; drop it to only log the diff
21
+ jobs:
22
+ check:
23
+ runs-on: ubuntu-latest
24
+ steps:
25
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
26
+ with:
27
+ persist-credentials: false
28
+ - uses: jimhoyd-com/urlcode/action@main # pin a release tag or commit
29
+ with:
30
+ expect-routes: 2
31
+ ```
32
+
33
+ The action lives at `action/action.yml` in the runtime repository, so the
34
+ reference is `jimhoyd-com/urlcode/action@<ref>`. Pin `<ref>` the way you pin
35
+ the runtime: a release tag or a commit SHA, not `main`, once the project is
36
+ past its first commit.
37
+
38
+ ## What it runs
39
+
40
+ | Step | Command | Fails the job when |
41
+ |---|---|---|
42
+ | Install | `npm ci` in the project (see below) | Dependencies do not install |
43
+ | Validate | `urlcode validate --project <project>` | The YAML, includes, functions or bindings do not load |
44
+ | Test | `urlcode test --project <project>` | A `tests/requests.json` fixture fails |
45
+ | Audit | `urlcode audit --project <project> --expect-routes N --compliance <profile>` | Count mismatch, failed generated check, uncovered active route/method, or a `high` compliance finding without `compliance-warn` |
46
+ | Route diff | `urlcode routes --compare base.json --format markdown` | Never; it reports |
47
+
48
+ Every command is the CLI documented in [readiness](READINESS.md) and
49
+ [compliance](COMPLIANCE.md); the action adds no check of its own. `--origin`
50
+ is passed to validate, test and audit when set. Steps run with `bash`, so the
51
+ action works on the Linux, macOS and Windows runners.
52
+
53
+ The project's runtime comes from the project. With a `package.json` the action
54
+ runs `npm ci` (or `npm install` without a lockfile) and uses the
55
+ `@jimhoyd/urlcode` that resolves from there, hoisted or not. A project without
56
+ `package.json`, such as a fresh `urlcode init`, gets the `runtime` input
57
+ installed into a private prefix under the runner's temp directory:
58
+ `@jimhoyd/urlcode` (the latest release) by default, or a version
59
+ (`@jimhoyd/urlcode@0.3.0`) or an absolute tarball path.
60
+
61
+ ## Inputs
62
+
63
+ | Input | Default | Meaning |
64
+ |---|---|---|
65
+ | `project` | `.` | Directory containing `urlcode.yaml`, relative to the workspace |
66
+ | `node-version` | `26` | Passed to `actions/setup-node` |
67
+ | `runtime` | `@jimhoyd/urlcode` | npm spec installed when the project has no `package.json` |
68
+ | `expect-routes` | empty | `audit --expect-routes N`; empty skips the count check |
69
+ | `compliance` | `baseline` | `baseline`, `strict`, `privacy` or `none` |
70
+ | `compliance-rules` | empty | Absolute path to an operator rules module outside the project |
71
+ | `compliance-warn` | `false` | Report findings without failing (`--compliance-warn`) |
72
+ | `origin` | empty | Public origin of the deployment under review |
73
+ | `route-diff` | `true` | Post the route diff on pull requests |
74
+
75
+ Output `route-diff` is the path of the Markdown diff on a pull request, empty
76
+ otherwise, for a later step that wants to upload or reuse it.
77
+
78
+ ## The sticky comment
79
+
80
+ On `pull_request` events the action fetches the base commit, checks it out into
81
+ a temporary git worktree, runs `urlcode routes` there and on the head with the
82
+ head's runtime, and renders `routes --compare` as Markdown: one table each for
83
+ added, removed and changed routes, or "No route changes". A changed route lists
84
+ each differing field (handler, methods, state, middleware count, policies,
85
+ generated marker or policy description) with its before and after value.
86
+
87
+ `action/comment.mjs` then finds the pull request's comments for the marker
88
+ `<!-- urlcode-route-diff project="<project>" -->` and updates that comment,
89
+ or creates it on the first run. The key is the project directory, so a
90
+ repository with several projects gets one comment per project and never a
91
+ pile of stale ones. Updating comments needs `pull-requests: write`; on a
92
+ fork's pull request or without that permission the API answers 403 or 404 and
93
+ the step logs a notice and exits 0, leaving the diff in the job log. The same
94
+ happens when the base commit is not reachable or its YAML does not load with
95
+ the head runtime. The diff is generic: only the action knows about GitHub.
96
+
97
+ ## Custom compliance rules
98
+
99
+ Write a rules module as [compliance](COMPLIANCE.md#writing-custom-rules)
100
+ describes and keep it outside the audited project, because it runs as trusted
101
+ host code. In a repository with the project at the root, put it in a sibling
102
+ directory and pass the absolute path:
103
+
104
+ ```yaml
105
+ - uses: jimhoyd-com/urlcode/action@main
106
+ with:
107
+ expect-routes: 25
108
+ compliance: strict
109
+ compliance-rules: ${{ github.workspace }}/ci/rules.mjs
110
+ compliance-warn: true
111
+ ```
112
+
113
+ `ci/rules.mjs` is inside the checkout but not inside the project only when
114
+ `project` is a subdirectory; with `project: .` keep the rules in a second
115
+ checkout or under `${{ runner.temp }}`. `compliance-rules` alone implies
116
+ `compliance: baseline`; `compliance: none` without rules skips the compliance
117
+ section entirely.
118
+
119
+ ## Exit codes
120
+
121
+ The job fails when any of validate, test or audit exits nonzero; the
122
+ [audit exit codes](COMPLIANCE.md#exit-codes) apply unchanged. The route diff
123
+ and the comment never fail the job. A failing install (missing `@jimhoyd/urlcode`
124
+ dependency, unavailable `runtime` spec) fails the job before any check runs.
125
+
126
+ ## The same checks locally
127
+
128
+ ```sh
129
+ urlcode validate --project .
130
+ urlcode test --project .
131
+ urlcode audit --project . --expect-routes 2 --compliance baseline
132
+ git stash && urlcode routes --project . > /tmp/base.json && git stash pop
133
+ urlcode routes --project . --compare /tmp/base.json --format markdown
134
+ ```
135
+
136
+ Or `make validate`, `make test` and `make audit ARGS='--expect-routes 2'`
137
+ from the starter Makefile. The runtime repository exercises the action on
138
+ every pull request against `examples/cookbook` (`.github/workflows/ci.yml`,
139
+ job `action`) with the packed tarball as `runtime`, and `test/action.test.ts`
140
+ checks that `action.yml` is a composite action with the inputs above and that
141
+ every third-party action it or the starter workflow uses is pinned to a
142
+ commit.
@@ -21,8 +21,7 @@ route pattern and the reason named:
21
21
 
22
22
  | Handler | Why it is refused |
23
23
  | --- | --- |
24
- | `function`, `middleware` | need worker threads and the QuickJS WASM engine |
25
- | `link` | needs a durable writable store the platform does not provide here |
24
+ | `function`, `middleware` | need the self-hosted Node lifecycle, and worker threads and the QuickJS WASM engine when a route declares `sandbox: true` |
26
25
  | `page`, `static`, `download` | need a platform static-asset binding, not an inline copy |
27
26
  | `env`, `secrets` | would have to be baked into the artifact |
28
27