@jimhoyd/urlcode 0.4.2 → 0.4.7

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 (214) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +57 -14
  2. package/.claude/skills/urlcode-operations/SKILL.md +4 -0
  3. package/README.md +42 -18
  4. package/SECURITY.md +5 -3
  5. package/dist/BUILD-MANIFEST.json +30 -27
  6. package/dist/agent-context.js +82 -0
  7. package/dist/agents-guide.js +42 -42
  8. package/dist/authoring.js +12 -2
  9. package/dist/body-schema.js +159 -0
  10. package/dist/build-cloudflare.js +2 -0
  11. package/dist/capabilities.js +1 -1
  12. package/dist/cli.js +28 -12
  13. package/dist/config.js +74 -6
  14. package/dist/context.js +4 -6
  15. package/dist/errors.js +3 -1
  16. package/dist/examples.js +1 -1
  17. package/dist/extensions.js +60 -2
  18. package/dist/http-policy.js +19 -4
  19. package/dist/http-response.js +2 -2
  20. package/dist/init-with.js +71 -9
  21. package/dist/mcp.js +15 -2
  22. package/dist/pattern-guard.js +32 -0
  23. package/dist/policies/security.js +0 -0
  24. package/dist/project-tests.js +35 -11
  25. package/dist/readiness.js +184 -32
  26. package/dist/recipes.js +1 -1
  27. package/dist/router.js +17 -0
  28. package/dist/runtime.js +26 -3
  29. package/dist/scaffold.js +0 -0
  30. package/dist/server.js +26 -3
  31. package/dist/site.js +0 -0
  32. package/dist/tooling.js +2 -1
  33. package/dist/types/agent-context.d.ts +44 -0
  34. package/dist/types/authoring.d.ts +3 -1
  35. package/dist/types/body-schema.d.ts +58 -0
  36. package/dist/types/config.d.ts +10 -2
  37. package/dist/types/context.d.ts +1 -1
  38. package/dist/types/errors.d.ts +9 -1
  39. package/dist/types/examples.d.ts +1 -1
  40. package/dist/types/extensions.d.ts +47 -2
  41. package/dist/types/http-policy.d.ts +3 -0
  42. package/dist/types/init-with.d.ts +10 -1
  43. package/dist/types/pattern-guard.d.ts +10 -0
  44. package/dist/types/project-tests.d.ts +9 -0
  45. package/dist/types/readiness.d.ts +68 -0
  46. package/dist/types/recipes.d.ts +1 -1
  47. package/dist/types/runtime.d.ts +4 -0
  48. package/dist/types/server.d.ts +9 -1
  49. package/dist/types/tooling.d.ts +1 -0
  50. package/dist/types/types.d.ts +13 -0
  51. package/dist/types.js +10 -2
  52. package/dist/typescript-authoring.js +5 -3
  53. package/dist/verify-deployment.js +16 -10
  54. package/examples/body-validation/README.md +16 -0
  55. package/examples/body-validation/example.yaml +17 -0
  56. package/examples/body-validation/tests/requests.json +15 -0
  57. package/examples/body-validation/urlcode.yaml +34 -0
  58. package/examples/coverage-waiver/README.md +8 -0
  59. package/examples/coverage-waiver/example.yaml +16 -0
  60. package/examples/coverage-waiver/functions/notes.mjs +2 -0
  61. package/examples/coverage-waiver/tests/requests.json +3 -0
  62. package/examples/coverage-waiver/urlcode.yaml +10 -0
  63. package/examples/data-dir/README.md +39 -0
  64. package/examples/data-dir/data/welcome.txt +1 -0
  65. package/examples/data-dir/example.yaml +22 -0
  66. package/examples/data-dir/functions/note.mjs +18 -0
  67. package/examples/data-dir/tests/requests.json +6 -0
  68. package/examples/data-dir/urlcode.yaml +6 -0
  69. package/examples/lifecycle/README.md +15 -0
  70. package/examples/lifecycle/example.yaml +19 -0
  71. package/examples/lifecycle/functions/notes.mjs +29 -0
  72. package/examples/lifecycle/tests/requests.json +12 -0
  73. package/examples/lifecycle/urlcode.yaml +29 -0
  74. package/examples/not-found/README.md +10 -0
  75. package/examples/not-found/example.yaml +17 -0
  76. package/examples/not-found/public/404.html +3 -0
  77. package/examples/not-found/public/index.html +3 -0
  78. package/examples/not-found/tests/requests.json +7 -0
  79. package/examples/not-found/urlcode.yaml +7 -0
  80. package/examples/shared-blocks/README.md +11 -0
  81. package/examples/shared-blocks/example.yaml +17 -0
  82. package/examples/shared-blocks/tests/requests.json +7 -0
  83. package/examples/shared-blocks/urlcode.yaml +37 -0
  84. package/llms-full.txt +364 -49
  85. package/llms.txt +44 -6
  86. package/package.json +15 -26
  87. package/recipes/static-page/README.md +9 -0
  88. package/recipes/static-page/public/index.html +11 -0
  89. package/recipes/static-page/recipe.yaml +21 -0
  90. package/recipes/static-page/tests/requests.json +22 -0
  91. package/recipes/static-page/urlcode.yaml +7 -0
  92. package/recipes/static-plus-api/README.md +6 -0
  93. package/recipes/static-plus-api/urlcode.yaml +4 -0
  94. package/recipes/store-crud/README.md +53 -0
  95. package/recipes/store-crud/recipe.yaml +31 -0
  96. package/recipes/store-crud/tests/requests.json +18 -0
  97. package/recipes/store-crud/urlcode.yaml +18 -0
  98. package/schemas/urlcode.schema.json +125 -60
  99. package/skills/urlcode/SKILL.md +47 -27
  100. package/starters/default/AGENTS.md +43 -43
  101. package/starters/page/README.md +14 -0
  102. package/starters/page/public/index.html +12 -0
  103. package/starters/page/tests/requests.json +17 -0
  104. package/starters/page/urlcode.yaml +6 -0
  105. package/.claude-plugin/marketplace.json +0 -18
  106. package/CONTRIBUTING.md +0 -148
  107. package/ROADMAP.md +0 -70
  108. package/docs/AI-AUTHORING.md +0 -339
  109. package/docs/ASSETS.md +0 -107
  110. package/docs/AUTH-BACKUP.md +0 -32
  111. package/docs/AWS.md +0 -86
  112. package/docs/BEST-PRACTICES.md +0 -276
  113. package/docs/BULK.md +0 -79
  114. package/docs/CAPABILITIES.md +0 -192
  115. package/docs/CAPACITY.md +0 -305
  116. package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
  117. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
  118. package/docs/CI.md +0 -147
  119. package/docs/CLOUDFLARE.md +0 -109
  120. package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
  121. package/docs/COMPLIANCE.md +0 -239
  122. package/docs/COMPOSING-A-SITE.md +0 -287
  123. package/docs/CONDITIONS.md +0 -74
  124. package/docs/CONTAINER-PROMOTION.md +0 -74
  125. package/docs/DEPLOYMENT-CHECKS.md +0 -108
  126. package/docs/DEVELOPMENT-PIPELINE.md +0 -304
  127. package/docs/EGRESS.md +0 -125
  128. package/docs/EXTENSIONS.md +0 -403
  129. package/docs/FRAMEWORK.md +0 -217
  130. package/docs/FUNCTION-SECURITY.md +0 -251
  131. package/docs/HTTP.md +0 -129
  132. package/docs/INSTALL.md +0 -128
  133. package/docs/INTERCHANGE.md +0 -134
  134. package/docs/LOAD-TESTING.md +0 -91
  135. package/docs/LOCAL-DEVELOPMENT.md +0 -102
  136. package/docs/MIDDLEWARE-EXAMPLES.md +0 -75
  137. package/docs/MIDDLEWARE.md +0 -102
  138. package/docs/MONITORING.md +0 -115
  139. package/docs/OBSERVABILITY.md +0 -222
  140. package/docs/OPEN-DECISIONS.md +0 -149
  141. package/docs/OPERATIONAL-PROOF.md +0 -41
  142. package/docs/OPERATIONS.md +0 -201
  143. package/docs/ORGANIZATION.md +0 -135
  144. package/docs/PERFORMANCE.md +0 -72
  145. package/docs/PLUGINS.md +0 -271
  146. package/docs/POLICIES.md +0 -211
  147. package/docs/PRERENDER.md +0 -245
  148. package/docs/PROJECT-DIRECTION.md +0 -118
  149. package/docs/PROVIDER-VERIFICATION.md +0 -84
  150. package/docs/READINESS.md +0 -153
  151. package/docs/README.md +0 -93
  152. package/docs/RECIPES.md +0 -99
  153. package/docs/RELEASE-0.4.0-alpha.3.md +0 -50
  154. package/docs/RELEASE-0.4.1.md +0 -73
  155. package/docs/RELEASE-0.4.2.md +0 -30
  156. package/docs/RELEASE-READINESS.md +0 -146
  157. package/docs/RELEASE-SECURITY.md +0 -100
  158. package/docs/RESILIENCE.md +0 -161
  159. package/docs/ROUTING.md +0 -92
  160. package/docs/SANDBOX-REVIEW.md +0 -72
  161. package/docs/SCAFFOLDING.md +0 -70
  162. package/docs/SECURITY-AUDIT.md +0 -164
  163. package/docs/SITE.md +0 -150
  164. package/docs/SPECIFICATION.md +0 -359
  165. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
  166. package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
  167. package/docs/SPIKE-CORE-LAYERING.md +0 -368
  168. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -207
  169. package/docs/STANDARDS.md +0 -311
  170. package/docs/STARTERS.md +0 -83
  171. package/docs/STATIC.md +0 -105
  172. package/docs/TOOLING.md +0 -298
  173. package/docs/TUNNELS.md +0 -72
  174. package/docs/TYPESCRIPT-AUTHORING.md +0 -87
  175. package/docs/TYPESCRIPT.md +0 -123
  176. package/docs/VERCEL.md +0 -114
  177. package/docs/VERSION-ALIGNMENT.md +0 -88
  178. package/docs/YAML-GUIDE.md +0 -57
  179. package/docs/YAML-REFERENCE.md +0 -449
  180. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
  181. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
  182. package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
  183. package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
  184. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
  185. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
  186. package/docs/archive/2026-09-19/ROADMAP.md +0 -387
  187. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
  188. package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
  189. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
  190. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
  191. package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
  192. package/docs/archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md +0 -116
  193. package/docs/archive/README.md +0 -29
  194. package/docs/policies/agents.md +0 -182
  195. package/docs/policies/cache.md +0 -152
  196. package/docs/policies/compression.md +0 -169
  197. package/docs/policies/contract.md +0 -52
  198. package/docs/policies/hardened.md +0 -56
  199. package/docs/policies/interoperability.md +0 -169
  200. package/docs/policies/operations.md +0 -45
  201. package/docs/policies/security.md +0 -161
  202. package/docs/policies/throttle.md +0 -103
  203. package/docs/yaml/assets.md +0 -36
  204. package/docs/yaml/conditions.md +0 -20
  205. package/docs/yaml/functions.md +0 -168
  206. package/docs/yaml/middleware.md +0 -31
  207. package/docs/yaml/organization.md +0 -74
  208. package/docs/yaml/policies.md +0 -37
  209. package/docs/yaml/redirects.md +0 -64
  210. package/docs/yaml/responses.md +0 -57
  211. package/docs/yaml/site.md +0 -24
  212. package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
  213. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -130
  214. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +0 -108
package/llms-full.txt CHANGED
@@ -1,16 +1,17 @@
1
1
  <!-- Generated by scripts/build-llms-full.ts (npm run docs:llms). Do not edit; edit the source documents. -->
2
- <!-- Consolidated URLCode authoring reference: 16 documents, about 52,118 tokens (estimate: characters / 4). -->
2
+ <!-- Consolidated URLCode authoring reference: 16 documents, about 57,753 tokens (estimate: characters / 4). -->
3
3
 
4
+ <!-- urlcode-current-version:start -->
4
5
  # URLCode
5
6
 
6
7
  > A portable runtime for programmable URL behavior, and the framework that grows
7
8
  > from it: routes in YAML, functions and middleware, then accounts, administration and
8
9
  > stored links as operator-installed extensions. Stable project format
9
- > `version: "1"`. Core is Apache-2.0; this revision is `0.4.0-alpha.2`, which makes
10
- > `function`/`middleware` routes trusted by default with `sandbox: true` as the
11
- > per-route opt-in; `0.4.0-alpha.1` is the newest alpha published to npm, on top of
12
- > the `0.3.0` release. The auth, admin and ui extension packages are on npm as
13
- > `0.1.0-alpha.x`, source-complete, review pending.
10
+ > `version: "1"`. Core is Apache-2.0; this revision is `0.4.7`. `function`/`middleware`
11
+ > routes are trusted by default with `sandbox: true` as the per-route opt-in. The
12
+ > auth, admin and ui extension packages in this repository are versioned at the same
13
+ > revision; confirm what is published with `npm run release:status`.
14
+ <!-- urlcode-current-version:end -->
14
15
 
15
16
  Use the schema and docs from the runtime revision you run. Do not assume Node
16
17
  or fetch inside a `sandbox: true` function, regex routes, database access,
@@ -59,10 +60,12 @@ claim here is implemented in the linked repository; nothing is roadmap.
59
60
  | `@jimhoyd/urlcode-auth` | [`packages/auth`](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth) | Accounts: password, passkeys, OpenID Connect, email codes, TOTP, recovery, sessions, roles, registration modes, account page, operator CLI | `extensions.auth` plus an `/account/*` mount and `policies.extensions.auth` on protected routes |
60
61
  | `@jimhoyd/urlcode-admin` | [`packages/admin`](https://github.com/jimhoyd-com/urlcode/blob/main/packages/admin) | Administration: users, sessions, roles, audit, registration approval, two-person cases, support impersonation, health | `extensions.admin` plus an `/admin/*` mount |
61
62
 
62
- All four are Apache-2.0. The `0.4.1` release line aligns their stable versions;
63
- check `npm run release:status` for publication progress. A stable npm channel is
64
- not an independent assessment: review, deployment evidence and an accessibility
65
- assessment are still pending
63
+ All four are Apache-2.0. Each package's npm `latest` tag identifies its stable
64
+ version. The current publisher records the exact four-package stack tested
65
+ together on each new GitHub release and attaches the same information in signed
66
+ `train.json` metadata.
67
+ A stable npm channel is not an independent assessment: review, deployment
68
+ evidence and an accessibility assessment are still pending
66
69
  ([issue 58](https://github.com/jimhoyd-com/urlcode/issues/58)). Their status
67
70
  files say exactly what is built: [auth](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/IMPLEMENTATION-STATUS.md),
68
71
  [admin](https://github.com/jimhoyd-com/urlcode/blob/main/packages/admin/IMPLEMENTATION-STATUS.md),
@@ -115,11 +118,12 @@ exact requirement.
115
118
 
116
119
  ### The composition contract
117
120
 
118
- After the aligned `0.4.1` packages are published, an extended project starts
119
- with the packages and one command:
121
+ An extended project starts from the current stable packages and one command.
122
+ `--save-exact` records the concrete versions selected from the independent
123
+ `latest` channels:
120
124
 
121
125
  ```sh
122
- npm install --save-exact @jimhoyd/urlcode@0.4.1 @jimhoyd/urlcode-ui@0.4.1 @jimhoyd/urlcode-auth@0.4.1 @jimhoyd/urlcode-admin@0.4.1
126
+ npm install --save-exact @jimhoyd/urlcode@latest @jimhoyd/urlcode-ui@latest @jimhoyd/urlcode-auth@latest @jimhoyd/urlcode-admin@latest
123
127
  urlcode init my-site --with ui,auth,admin
124
128
  ```
125
129
 
@@ -178,12 +182,22 @@ export default {
178
182
  };
179
183
  ```
180
184
 
181
- Auth and admin already render their screens through `ui.kit` when the host
182
- supplies the UI extension; their package-owned templates and catalogues must be
183
- registered with that kit. Without it, both retain shared primitive rendering
184
- through `presentation`. The example above uses that primitive fallback. The UI
185
- block is optional, and a migration to the kit is not unfinished framework work.
186
- See each package README for its complete kit registration example.
185
+ Auth and admin render every screen through `ui.kit`; their package-owned
186
+ templates and catalogues must be registered with that kit. Both refuse
187
+ activation when the UI extension is absent or has not activated first. See each
188
+ package README for the complete registration example; the scaffold generated by
189
+ `urlcode init --with ui,auth,admin` wires the same composition.
190
+
191
+ Treat that composition as one application with package ownership boundaries,
192
+ not as three separate user interfaces. Keep identity, session and recovery
193
+ behavior in auth and authorization, freshness, auditing and mutations in admin.
194
+ Apply product differences through the installed extensions' declared authoring
195
+ surfaces, in this order: configuration and theme, copy, a component or screen
196
+ template, stylesheet, then a supported hook. Create another extension only for
197
+ a reusable capability those surfaces cannot express. `urlcode extensions`
198
+ and the MCP `get_extensions` tool report those surfaces and their fast checks,
199
+ so people and agents can discover the supported path instead of replacing
200
+ package behavior.
187
201
 
188
202
  ```sh
189
203
  urlcode serve --project /absolute/site --host-file /absolute/operator/host.mjs --origin https://site.example
@@ -220,6 +234,12 @@ These are the facts that keep generated projects valid. The full matrix is in
220
234
  Function and middleware `source` fields do name project modules. Extensions are
221
235
  logical names; the host file chooses the implementation. There is no
222
236
  `--extension` flag, no `import` in YAML, no interpolation.
237
+ - **Build one product through declared authoring surfaces.** Inspect extension
238
+ authoring metadata before generating code. Prefer configuration/theme, copy,
239
+ the smallest template override, stylesheet and supported hooks, in that
240
+ order. Add an extension only for a reusable missing capability. Run the
241
+ reported fast checks while iterating and the full repository checks before
242
+ handoff.
223
243
  - **One handler per route.** `redirect`, `respond`, `page`, `static`, `download`,
224
244
  `function`, `proxy`, `conditional` or `extension`, plus optional
225
245
  `middleware`. Paths are exact or single-segment `{param}`; `/*` only on
@@ -258,7 +278,7 @@ These are the facts that keep generated projects valid. The full matrix is in
258
278
  | Add accounts | [auth README](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/README.md), [auth security](https://github.com/jimhoyd-com/urlcode/blob/main/packages/auth/SECURITY.md) |
259
279
  | Add administration | [admin README](https://github.com/jimhoyd-com/urlcode/blob/main/packages/admin/README.md) |
260
280
  | Restyle every page | [ui README](https://github.com/jimhoyd-com/urlcode/blob/main/packages/ui/README.md), [ui contract](https://github.com/jimhoyd-com/urlcode/blob/main/packages/ui/CONTRACT.md) |
261
- | Write an extension | [extensions](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md), [extension model review](https://github.com/jimhoyd-com/urlcode/blob/main/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md) |
281
+ | Write an extension | [extensions](https://github.com/jimhoyd-com/urlcode/blob/main/docs/EXTENSIONS.md) |
262
282
  | Run it | [operations](https://github.com/jimhoyd-com/urlcode/blob/main/docs/OPERATIONS.md), [install](https://github.com/jimhoyd-com/urlcode/blob/main/docs/INSTALL.md), [deployment checks](https://github.com/jimhoyd-com/urlcode/blob/main/docs/DEPLOYMENT-CHECKS.md) |
263
283
 
264
284
  ---
@@ -267,12 +287,29 @@ These are the facts that keep generated projects valid. The full matrix is in
267
287
 
268
288
  Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/AI-AUTHORING.md
269
289
 
270
- Use this as project-authoring context. It describes the implemented source contract, including unreleased additions after 0.3.0,
290
+ Use this as project-authoring context. It describes the implemented source contract, including additions since 0.3.0,
271
291
  not a general server framework. Runtime/schema/docs
272
292
  must come from the same reviewed revision. The runtime is Apache-2.0; a
273
293
  project you generate carries whatever license its owner chooses, so do not
274
294
  add one to it automatically.
275
295
 
296
+ ### First step: one bounded query
297
+
298
+ Make the first retrieval one bounded query: the MCP tool `get_context` when the
299
+ `urlcode` server is registered, otherwise `urlcode context --project DIR` (add
300
+ `--budget N` when context is scarce). Then retrieve only what the task needs:
301
+ `urlcode capabilities NAME` (MCP `get_capability`) for one capability's limits,
302
+ `get_schema` for one YAML fragment, `urlcode recipes search TEXT`
303
+ (`search_recipes`), `explain` for a route's effective behavior and, when the
304
+ operator supplies a host file, `get_extensions`. Context is a summary with the
305
+ constraints and exact commands, not a schema dump, and it never hides a
306
+ capability limit: ask `capabilities NAME` before promising a feature.
307
+
308
+ The complete catalogs (`urlcode capabilities`, `recipes list`), the compact
309
+ [llms.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms.txt) index and the generated [llms-full.txt](https://github.com/jimhoyd-com/urlcode/blob/main/llms-full.txt)
310
+ stay available as deliberate fallback and reference, not as the opening move.
311
+ The reading order below is for that reference use.
312
+
276
313
  ### Declarative-first default
277
314
 
278
315
  > Use URLCode's highest-level declarative features whenever possible. Generate custom code only when the framework cannot express the requirement.
@@ -316,18 +353,31 @@ as evidence, and the rules on grants and secrets. Assistants that load skills
316
353
  find the same loop in `skills/urlcode/SKILL.md` inside the installed package; it
317
354
  teaches how to retrieve the minimum reference through `urlcode capabilities`,
318
355
  `urlcode recipes list|show` and `urlcode validate --local` rather than reading
319
- the documentation whole. Neither file replaces the schema; both defer to it.
356
+ the documentation whole. For a host-composed application, `get_extensions`
357
+ adds each extension's schemas, hooks, supported authoring surfaces and fast
358
+ checks. Agents should use those surfaces before generating replacement package
359
+ behavior. Neither file replaces the schema; both defer to it.
360
+
361
+ Treat core, installed extensions and product UI as one application with
362
+ different owners. Keep auth/admin security and workflow behavior package-owned;
363
+ keep branding, product navigation and the smallest necessary overrides in the
364
+ project. When a React frontend contains `components.json`, use the installed
365
+ official shadcn/ui skill for component discovery, composition, accessibility
366
+ and semantic Tailwind styling: start with `shadcn info --json`, then use its
367
+ documentation/search flow or configured MCP registry before generating a
368
+ component. The server template kit is shadcn-compatible but does not accept
369
+ React components. See the official [shadcn/ui skills guide](https://ui.shadcn.com/docs/skills).
320
370
 
321
371
  ### Authoring workflow
322
372
 
323
- Run `urlcode context --project ./my-links` first. It prints, in a few hundred
324
- tokens, the runtime and schema version, what the project already uses, the
373
+ Start with the bounded query above. It prints the runtime and schema version, what the project already uses, the
325
374
  constraints that hold for every project, which targets refuse this project's
326
375
  features and the exact validate, test and audit commands with the intentional
327
376
  route count filled in. It is derived from the compiled project and the
328
377
  capability catalog, never from prose, so prefer it to re-reading the
329
378
  documentation; add `--budget N` when context is scarce and `--json` for
330
- tooling. The same data is available from the MCP tool `get_context`.
379
+ tooling. Its size grows with the project (about a thousand estimated tokens for
380
+ the starter, a few thousand for the cookbook), not with the framework.
331
381
 
332
382
  - Inspect the existing entry point, included files, functions, tests and pinned
333
383
  runtime. Preserve the user's organization and unrelated routes.
@@ -361,6 +411,26 @@ Use the intentional actual count, not always 2. Runtime checkout users can repla
361
411
  External bindings require an already reviewed policy; add `--policy` where needed.
362
412
  The benchmark operates locally; it is not a load test of an external deployment.
363
413
 
414
+ ### Feedback from real authoring work
415
+
416
+ The authoring loop is also a source of roadmap evidence. After completing a
417
+ real task, an assistant should make a feedback **draft** only when a concrete
418
+ attempt exposes a capability gap, repeated framework workaround,
419
+ documentation/discovery gap, or suspected defect. One-off product logic is not
420
+ framework feedback.
421
+
422
+ The draft must name the installed runtime version and requested target, then
423
+ include a sanitized route/YAML fragment, the exact validation or test
424
+ observation, the smallest expected behavior, and a proposed fixture. It must
425
+ not include secrets, customer URLs, raw application source, or unsupported
426
+ claims inferred from a failed attempt.
427
+
428
+ Search [existing URLCode issues](https://github.com/jimhoyd-com/urlcode/issues)
429
+ before proposing a new one and identify a likely duplicate when found. An agent
430
+ may present a draft issue or comment for review, but must never create or update
431
+ a GitHub issue without the user's explicit approval. Feedback is evidence for a
432
+ maintainer to review; it is not a promise that the public contract will grow.
433
+
364
434
  ### Capability matrix: do not hallucinate these features
365
435
 
366
436
  | Available | Unavailable or future |
@@ -406,11 +476,49 @@ declared route at the same path still wins. Count its generated routes in
406
476
  `--expect-routes`. `site.sitemap` needs `--origin` at every command that
407
477
  activates the project; see [site conventions](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SITE.md).
408
478
 
479
+ ### Built-in features by task
480
+
481
+ Before writing a function, check whether a declarative feature already covers the
482
+ need. Security headers are the usual miss: a project that declares nothing sends
483
+ only the runtime's defaults (`nosniff`, `no-store`, a request ID).
484
+
485
+ | I need | Declare | Reference |
486
+ |---|---|---|
487
+ | Security headers (CSP, HSTS, frame and referrer policy) | `policies.security: {headers: oshp}` or `policies.profile: hardened` | [security](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/security.md) |
488
+ | Cache headers on a page, download or static mount | `cacheControl`: `no-cache` (default), `no-store`, `public, max-age=3600` or `public, max-age=31536000, immutable`; nothing else validates | [assets](https://github.com/jimhoyd-com/urlcode/blob/main/docs/yaml/assets.md) |
489
+ | A cache strategy on any route | `policies.cache` | [cache](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/cache.md) |
490
+ | Body size, required body, content types, JSON syntax and shape | `request.body.maxBytes`, `required`, `contentTypes`, `format`, `schema` | [HTTP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md#body-schema-and-input-patterns) |
491
+ | A uuid path id or a bounded string pattern | parameter `schema: {type: string, format: uuid}` or `pattern` with `maxLength` | [HTTP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md#body-schema-and-input-patterns) |
492
+ | Method gating | `methods` (default GET/HEAD; 405 with `Allow`) | [HTTP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md) |
493
+ | Rate limits, bot and crawler denial, compression | `policies.throttle`, `agents`, `compression` | [policies](https://github.com/jimhoyd-com/urlcode/blob/main/docs/POLICIES.md) |
494
+ | Static JSON or text and fixed headers | `respond`, `response.headers` | [HTTP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md) |
495
+ | robots, sitemap, favicon, security.txt, llms.txt | top-level `site` | [site](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SITE.md) |
496
+
497
+ Which handler serves the response:
498
+
499
+ | The response is | Handler | Recipe |
500
+ |---|---|---|
501
+ | Fixed text or JSON | `respond` | `health-page` |
502
+ | A short HTML snippet | `respond` `text` plus `response.headers` `Content-Type: text/html; charset=utf-8` | [HTTP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md) |
503
+ | One HTML file | `page` | `static-page` |
504
+ | A directory of files | `static` | `static-plus-api` |
505
+ | An attachment | `download` | `protected-download` |
506
+
507
+ Data persistence has no native handler. The operator-installed `store` extension
508
+ serves declared collections as a CRUD API, and `urlcode recipes search "crud store
509
+ persist"` finds the `store-crud` recipe. It needs the operator to install
510
+ `@jimhoyd/urlcode-store` (on npm) and a host file. `init --with
511
+ ui,auth,store` scaffolds one from the published packages; a no-auth
512
+ `--with store` needs `--ack store:public-write`, which only a core release after the
513
+ store's first publication has, so say so rather than promising it. Report anything beyond that recipe (filtering, sorting, per-record
514
+ ownership, a database) as a gap. `urlcode context` lists the same built-ins so
515
+ they are visible before you write code.
516
+
409
517
  ### Agent skills
410
518
 
411
519
  This repository ships two agent skills, each a thin trigger pointing at the
412
- docs that are the actual source of truth, so there is one place to keep
413
- current rather than two:
520
+ version-matched CLI/schema and documentation bundle that are the actual source
521
+ of truth, so there is one place to keep current rather than two:
414
522
 
415
523
  - [`urlcode-authoring`](https://github.com/jimhoyd-com/urlcode/blob/main/.claude/skills/urlcode-authoring/SKILL.md) — this
416
524
  guide, the schema and the reference. It loads the capability limits and the
@@ -431,7 +539,10 @@ Three ways to get either, all pinned to a runtime revision:
431
539
  - **npm.** The published package includes both skill directories. Copy the
432
540
  one(s) you want into your project's `.claude/skills/` to pin guidance to the
433
541
  same revision as the runtime you installed; a skill inside `node_modules` is
434
- not discovered on its own.
542
+ not discovered on its own. The npm archive keeps `llms-full.txt` as its one
543
+ offline prose bundle instead of duplicating the repository's `docs/` tree;
544
+ use the CLI for structured queries and search that bundle by document heading
545
+ when more explanation is needed.
435
546
  - **Plugin marketplace.** `.claude-plugin/marketplace.json` publishes the
436
547
  `packaging/claude-plugin` distribution from this repository, carrying both
437
548
  skills. Add the marketplace by its Git URL and install the `urlcode` plugin.
@@ -441,7 +552,7 @@ Three ways to get either, all pinned to a runtime revision:
441
552
 
442
553
  `npm run docs:plugin` regenerates the plugin distribution from both skills;
443
554
  `npm run check` fails if it is stale or if either skill names a documentation
444
- path this revision does not ship.
555
+ path that does not exist in this revision.
445
556
  ### Bounded authoring tools
446
557
 
447
558
  Before generating a common route by hand, search the bundled catalog:
@@ -536,7 +647,11 @@ under `advisories` with "consider whether this route needs `sandbox: true`".
536
647
  This is a nudge to look, the same advisory spirit as the rest of `audit`'s
537
648
  non-blocking findings — it never fails the check, never sets `ready: false`
538
649
  and never infers the actual answer; setting `sandboxReason` (with `sandbox`
539
- either `true` or `false`) or `sandbox: true` is enough to silence it.
650
+ either `true` or `false`) or `sandbox: true` is enough to silence it. The
651
+ advisory prints the exact line to add. Anything that touches the filesystem
652
+ (a persistent app writing files, for example) must be a trusted route,
653
+ because a sandbox has no filesystem: declare `sandboxReason` with the default
654
+ `sandbox: false` and say why it is trusted, as the `static-plus-api` recipe does.
540
655
 
541
656
  The same judgment call applies to a project-level lifecycle hook an
542
657
  extension invokes (`onSignUp`, `beforeRegister` and the like) — it is
@@ -611,10 +726,12 @@ programmatic compatibility analysis and provider verification limits.
611
726
 
612
727
  Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/YAML-GUIDE.md
613
728
 
614
- This guide targets URLCode 0.3.0. Start with the function example below,
729
+ <!-- urlcode-current-version:start -->
730
+ This guide targets URLCode 0.4.7. Start with the function example below,
615
731
  then add only the fields your route needs. The authoritative machine-readable
616
732
  shape is [JSON Schema](https://github.com/jimhoyd-com/urlcode/blob/main/schemas/urlcode.schema.json); semantic rules are in the
617
733
  [specification](https://github.com/jimhoyd-com/urlcode/blob/main/docs/SPECIFICATION.md). Unsupported fields fail validation.
734
+ <!-- urlcode-current-version:end -->
618
735
 
619
736
  ### Run all the examples
620
737
 
@@ -694,6 +811,8 @@ schema-valid combinations activate successfully.
694
811
  | `routes.*.enabled` | boolean | no | — |
695
812
  | `routes.*.sandbox` | boolean | no | default: false |
696
813
  | `routes.*.sandboxReason` | string | no | maxLength: 500 |
814
+ | `routes.*.coveredElsewhere` | object | no | minProperties: 1 |
815
+ | `routes.*.coveredElsewhere.*` | string | no | minLength: 1; maxLength: 500 |
697
816
  | `routes.*.expires` | string | no | — |
698
817
  | `routes.*.description` | string | no | maxLength: 1024 |
699
818
  | `routes.*.parameters` | array | no | maxItems: 64 |
@@ -711,6 +830,8 @@ schema-valid combinations activate successfully.
711
830
  | `routes.*.parameters[].schema.maxLength` | integer | no | minimum: 0; maximum: 8192 |
712
831
  | `routes.*.parameters[].schema.minimum` | number | no | — |
713
832
  | `routes.*.parameters[].schema.maximum` | number | no | — |
833
+ | `routes.*.parameters[].schema.pattern` | string | no | minLength: 1; maxLength: 128 |
834
+ | `routes.*.parameters[].schema.format` | string | no | enum: ["uuid"] |
714
835
  | `routes.*.parameters[].schema.items` | object | no | unknown keys rejected |
715
836
  | `routes.*.parameters[].schema.items.type` | string | yes | enum: ["string","integer","number","boolean"] |
716
837
  | `routes.*.parameters[].schema.maxItems` | integer | no | minimum: 0; maximum: 100 |
@@ -771,6 +892,7 @@ schema-valid combinations activate successfully.
771
892
  | `routes.*.request.body.contentTypes` | array | no | minItems: 1; maxItems: 16; uniqueItems: true |
772
893
  | `routes.*.request.body.contentTypes[]` | string | no | pattern: "^[a-z0-9!#$&^_.+-]+/[a-z0-9!#$&^_.+-]+$" |
773
894
  | `routes.*.request.body.format` | string | no | enum: ["text","json"] |
895
+ | `routes.*.request.body.schema` | object | no | — |
774
896
  | `routes.*.response` | object | no | unknown keys rejected |
775
897
  | `routes.*.response.headers` | object | no | maxProperties: 64 |
776
898
  | `routes.*.response.headers.*` | one of the shapes below | no | — |
@@ -951,6 +1073,7 @@ schema-valid combinations activate successfully.
951
1073
  | `routes.*.cache.maxBytes` | integer | no | minimum: 0; maximum: 16777216 |
952
1074
  | `routes.*.cache.maxEntries` | integer | no | minimum: 1; maximum: 1000000 |
953
1075
  | `routes.*.cache.force` | boolean | no | default: false |
1076
+ | `routes.*.use` | string | no | pattern: "^[a-z][a-z0-9-]{0,63}$" |
954
1077
  | `includes` | array | no | maxItems: 256; uniqueItems: true |
955
1078
  | `includes[]` | string | no | maxLength: 1024 |
956
1079
  | `policies` | object | no | unknown keys rejected |
@@ -1115,10 +1238,27 @@ schema-valid combinations activate successfully.
1115
1238
  | `site.securityTxt.encryption` | array | no | maxItems: 64 |
1116
1239
  | `site.securityTxt.encryption[]` | string | no | minLength: 1; maxLength: 2048 |
1117
1240
  | `site.llms` | string | no | minLength: 1; maxLength: 1024 |
1241
+ | `site.notFound` | string | no | minLength: 1; maxLength: 1024; pattern: "\\.[hH][tT][mM][lL]?$" |
1118
1242
  | `extensions` | object | no | maxProperties: 16 |
1119
1243
  | `extensions.*` | object | no | unknown keys rejected |
1120
1244
  | `extensions.*.version` | constant | yes | const: "1" |
1121
1245
  | `extensions.*.config` | object | yes | — |
1246
+ | `shared` | object | no | maxProperties: 32 |
1247
+ | `shared.*` | object | no | unknown keys rejected |
1248
+ | `shared.*.request` | object | no | unknown keys rejected |
1249
+ | `shared.*.request.body` | object | no | unknown keys rejected |
1250
+ | `shared.*.request.body.required` | boolean | no | — |
1251
+ | `shared.*.request.body.maxBytes` | integer | no | minimum: 0; maximum: 1048576 |
1252
+ | `shared.*.request.body.contentTypes` | array | no | minItems: 1; maxItems: 16; uniqueItems: true |
1253
+ | `shared.*.request.body.contentTypes[]` | string | no | pattern: "^[a-z0-9!#$&^_.+-]+/[a-z0-9!#$&^_.+-]+$" |
1254
+ | `shared.*.request.body.format` | string | no | enum: ["text","json"] |
1255
+ | `shared.*.request.body.schema` | object | no | — |
1256
+ | `shared.*.response` | object | no | unknown keys rejected |
1257
+ | `shared.*.response.headers` | object | no | maxProperties: 64 |
1258
+ | `shared.*.response.headers.*` | one of the shapes below | no | — |
1259
+ | `shared.*.response.headers.* (option 1)` | string | no | maxLength: 4096 |
1260
+ | `shared.*.response.headers.* (option 2)` | array | no | minItems: 1; maxItems: 16 |
1261
+ | `shared.*.response.headers.* (option 2)[]` | string | no | maxLength: 4096 |
1122
1262
 
1123
1263
  ---
1124
1264
 
@@ -1127,7 +1267,7 @@ schema-valid combinations activate successfully.
1127
1267
  Source: https://github.com/jimhoyd-com/urlcode/blob/main/docs/SPECIFICATION.md
1128
1268
 
1129
1269
  This document and [JSON Schema](https://github.com/jimhoyd-com/urlcode/blob/main/schemas/urlcode.schema.json) describe
1130
- the source contract, including unreleased additions after 0.3.0.
1270
+ the source contract, including additions since 0.3.0.
1131
1271
  `version: "1"` remains the project-format contract. Unsupported fields
1132
1272
  are rejected rather than interpreted as future behavior.
1133
1273
 
@@ -1232,6 +1372,31 @@ Routes accept `request.body` validation and `response.headers` overrides. The
1232
1372
  See [HTTP configuration](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md) for the exact supported fields, precedence,
1233
1373
  security restrictions and examples.
1234
1374
 
1375
+ ### Shared blocks
1376
+
1377
+ An optional top-level `shared` map (entry `urlcode.yaml` only; at most 32 names
1378
+ matching `^[a-z][a-z0-9-]{0,63}$`) holds named `request` and `response.headers`
1379
+ blocks. A route selects one with `use: <name>`. This is the reuse mechanism;
1380
+ anchors, aliases and merge keys stay rejected. Rules:
1381
+
1382
+ - A route's own `request` (or `response`) key replaces the shared block's key as
1383
+ a whole. There is no deep merge, so a reviewer never reconstructs a header set
1384
+ from two places.
1385
+ - Resolution happens at load time, before the route hash, `audit`, `routes` and
1386
+ every compiler run. The resolved route carries no `use`, and changing a shared
1387
+ block changes the hash of exactly the routes that select it.
1388
+ - An unknown name fails validation, as does a shared block whose
1389
+ `response.headers` name a header the runtime owns (the same set a route may
1390
+ not set). Included files may `use` names from the entry file but may not
1391
+ declare `shared`. Per-route rules, such as the asset-handler header limits,
1392
+ still apply to the resolved route.
1393
+ - Only `request` and `response.headers` are shareable. `sandbox`,
1394
+ `sandboxReason`, `policies` and other route defaults are deliberately left
1395
+ out for now, so each trust decision stays on its route.
1396
+ - No cross-file or remote reuse of blocks.
1397
+
1398
+ See the [shared-blocks example](https://github.com/jimhoyd-com/urlcode/blob/main/examples/shared-blocks/README.md).
1399
+
1235
1400
  ### Policies
1236
1401
 
1237
1402
  Optional top-level `policies` and `profiles` keys, and `routes.<path>.policies`,
@@ -1254,8 +1419,11 @@ Duplicate scalar query/header inputs return 400. Required missing inputs return
1254
1419
  400; defaults apply only to absent inputs. Empty strings are present values.
1255
1420
 
1256
1421
  Supported validation: `type`, scalar `enum`, `default`, string `minLength`/
1257
- `maxLength`, numeric `minimum`/`maximum`, query array `items` and `maxItems`.
1258
- Regular-expression patterns, structured bodies, cookies, nested inputs and
1422
+ `maxLength`, string `format: uuid` and a bounded `pattern` ([restrictions](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md#body-schema-and-input-patterns)),
1423
+ numeric `minimum`/`maximum`, query array `items` and `maxItems`. JSON bodies are
1424
+ validated by `request.body.schema`, not by a parameter (a failure answers 422, as JSON
1425
+ when the client explicitly accepts `application/json` and otherwise as text; see
1426
+ [HTTP](https://github.com/jimhoyd-com/urlcode/blob/main/docs/HTTP.md#body-schema-and-input-patterns)). Cookies, nested inputs and
1259
1427
  OpenAPI `style`/`explode` fields are not implemented. This uses a documented
1260
1428
  OpenAPI-like input subset; it is not an OpenAPI document or full JSON Schema
1261
1429
  input vocabulary. String limits are at most 8,192 characters.
@@ -1328,6 +1496,16 @@ entry may likewise be a string, normalized to `{source: <string>}`. Only the
1328
1496
  long form exists after loading, so `routes`, `audit`, `explain`, revision hashes
1329
1497
  and the field reference describe the expansion.
1330
1498
 
1499
+ Auto-binding belongs to the string form only. The long form (`source`, and
1500
+ therefore any `export:`) is taken as written: URLCode does not add `args` or
1501
+ path `parameters` to it, so a route like `/api/todos/{id}` with
1502
+ `function: {source: ..., export: get}` must declare the `id` parameter under
1503
+ `parameters` and map it under `args` by hand, as the example above does. A
1504
+ function has one `source` and one `export` per route; to serve several methods
1505
+ on one path, branch on `request.method` inside the function, or declare one
1506
+ route per method where the path allows it. A `methods:` map of per-method
1507
+ functions is not implemented.
1508
+
1331
1509
  ES modules only. `.mjs` is loaded as ESM in both modes. A `sandbox: true` route
1332
1510
  also reads `.js` as ESM independently of Node package settings, but a trusted
1333
1511
  route imports `.js` through Node's own loader, so an ESM `.js` module needs the
@@ -1646,6 +1824,7 @@ status or default `Cache-Control: no-store` on functions/redirects.
1646
1824
  | `request.body.maxBytes` | 0–1048576; tighter per-route budget, enforced while reading fixed/chunked bodies; 413 on overflow |
1647
1825
  | `request.body.contentTypes` | Exact lowercase MIME essences for nonempty bodies; parameters ignored; mismatch/missing type returns 415 |
1648
1826
  | `request.body.format` | `text`: validate UTF-8; `json`: validate UTF-8, JSON media type and JSON syntax; malformed input returns 400 |
1827
+ | `request.body.schema` | Requires `format: json`. A JSON Schema subset checked after parsing; a body that breaks it returns 422 (see below) |
1649
1828
 
1650
1829
  The operator request limit remains an upper bound; YAML cannot raise it. A route
1651
1830
  without body policy keeps the existing server limit. A configured body policy
@@ -1685,12 +1864,76 @@ cache and disposition on its own handler. Asset metadata cannot be overridden by
1685
1864
  `response.headers`. On functions/declared responses, Content-Type may be configured;
1686
1865
  JSON declarations require a JSON type. No response header secret interpolation.
1687
1866
 
1867
+ #### Body schema and input patterns
1868
+
1869
+ `request.body.schema` accepts `type` (`object`, `array`, `string`, `integer`,
1870
+ `number`, `boolean`, `null`), `properties`, `required`, `additionalProperties`
1871
+ (true or false), `items`, scalar `enum`, `minLength`/`maxLength`, `pattern`,
1872
+ `format: uuid`, `minimum`/`maximum` and `minItems`/`maxItems`. Anything else,
1873
+ including `$ref`, `oneOf` and `default`, fails activation. A schema is limited to
1874
+ 6 levels, 128 nodes and 64 properties per object. It is checked by the runtime
1875
+ itself, so it behaves the same on every host and is not compiled from author
1876
+ code.
1877
+
1878
+ A failing body answers **422** as `text/plain`: `Request body failed validation`
1879
+ then one line per failure, at most 8, each naming only a path the schema
1880
+ declared (`/title must be a string`). Array positions print as `[]`. Nothing the
1881
+ client sent is echoed, in line with the fixed-words rule for runtime errors.
1882
+ Malformed JSON stays 400 and a wrong media type 415.
1883
+
1884
+ A client that sends `Accept: application/json` gets the same failures as
1885
+ `application/json` instead (the server and the Cloudflare Worker agree):
1886
+
1887
+ ```json
1888
+ {"error":"body_validation_failed","message":"Request body failed validation","issues":[{"pointer":"/title","keyword":"maxLength","message":"must be at most 8 characters","expected":8}]}
1889
+ ```
1890
+
1891
+ Each issue carries `pointer` (RFC 6901, built only from names the schema
1892
+ declared; array positions are `/[]`, not an index; the root is `""`), `keyword`
1893
+ (`type`, `enum`, `required`, `additionalProperties`, `minLength`, `maxLength`,
1894
+ `format`, `pattern`, `minimum`, `maximum`, `minItems` or `maxItems`), the fixed
1895
+ `message`, and where the schema states one, `expected` (the type, bound, format
1896
+ or, for `enum`, up to 16 short declared values) or `property` (the missing name
1897
+ from `required`). The offending value is never included, because it may hold a
1898
+ secret. At most 8 issues are listed and the body is capped at 4096 bytes;
1899
+ when trailing issues are dropped to fit, `"truncated":true` is added.
1900
+
1901
+ Negotiation is deliberately conservative: JSON is sent only when the Accept
1902
+ header names `application/json` explicitly with `q` above 0 and no higher `q` for
1903
+ an explicit `text/plain`. A missing header, `*/*`, `application/*`, browsers'
1904
+ default Accept and a malformed `q` keep the plain-text answer. The status is 422
1905
+ either way and the same checks run before any function or sandbox code.
1906
+
1907
+ Parameter schemas (path, query, header) also accept `format: uuid` and `pattern`
1908
+ on string inputs, rejecting a mismatch with 400. `pattern` runs on every request
1909
+ in the host process, so it is restricted: 1 to 128 characters, `maxLength` of at
1910
+ most 128 on the same schema, no group repeated by `*`, `+` or `{n,}`, no
1911
+ lookaround, no backreference and at most three unbounded quantifiers. That
1912
+ restriction is conservative, not a proof of linear time. It is what stands
1913
+ between an author regex and a backtracking stall, so prefer `format` or `enum`
1914
+ when either fits.
1915
+
1688
1916
  `respond` is an additional native handler (exactly one handler per route):
1689
1917
 
1690
1918
  - `status`: 200–599, default 200; 206 and 304 are reserved for native asset semantics.
1691
1919
  - `text`: literal UTF-8 body, default content type text/plain.
1692
1920
  - `json`: any JSON-compatible YAML value, serialized with application/json.
1693
1921
  - Omit both for an empty body; declaring both fails. Body limit is 1 MiB.
1922
+ - A short HTML answer is `text` plus a declared content type. There is no
1923
+ `respond.html`; use the `page` handler for anything larger than a snippet:
1924
+
1925
+ ```yaml
1926
+ /:
1927
+ respond:
1928
+ text: "<!doctype html><h1>Hello</h1>"
1929
+ response:
1930
+ headers:
1931
+ Content-Type: text/html; charset=utf-8
1932
+ ```
1933
+
1934
+ The body is served verbatim and the default `nosniff` and `no-store` still
1935
+ apply. Under the `oshp` security profile the CSP (`default-src 'self'`) blocks
1936
+ inline `<script>` and `<style>`, so keep the snippet to markup.
1694
1937
  - Status 204/205 cannot declare a nonempty body. HEAD always suppresses the body.
1695
1938
 
1696
1939
  Functions still return their own Response/status/body. YAML header policy does
@@ -1960,10 +2203,8 @@ compiled and the request path is unchanged. Each policy has its own page:
1960
2203
  | `compression` | [compression](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/compression.md) | `Accept-Encoding` negotiation; assets precompressed at snapshot time |
1961
2204
  | `cache` | [cache](https://github.com/jimhoyd-com/urlcode/blob/main/docs/policies/cache.md) | Named strategies from RFC 9111/5861/8246/9213 plus an origin memory cache |
1962
2205
 
1963
- The design and the reasoning behind each choice are in the
1964
- [extensions spike](https://github.com/jimhoyd-com/urlcode/blob/main/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md). Operators who need behavior the
1965
- declarative block cannot express pass host [plugins](https://github.com/jimhoyd-com/urlcode/blob/main/docs/PLUGINS.md) in code; plugins
1966
- are never named in YAML.
2206
+ Operators who need behavior the declarative block cannot express pass host
2207
+ [plugins](https://github.com/jimhoyd-com/urlcode/blob/main/docs/PLUGINS.md) in code; plugins are never named in YAML.
1967
2208
 
1968
2209
  ### Declaring policies
1969
2210
 
@@ -2182,6 +2423,7 @@ site:
2182
2423
  policy: [https://example.com/security-policy]
2183
2424
  preferredLanguages: [en, fr]
2184
2425
  llms: llms.txt
2426
+ notFound: public/404.html
2185
2427
  routes:
2186
2428
  /: {page: {file: public/index.html}}
2187
2429
  ```
@@ -2291,9 +2533,30 @@ refused).
2291
2533
  A project-relative text file served as a `page` route with
2292
2534
  `text/plain; charset=utf-8` and the default `no-cache`.
2293
2535
 
2536
+ #### `notFound` → `/404.html`
2537
+
2538
+ A project-relative `.html`/`.htm` file that answers a request matching no
2539
+ route. It becomes a `page` route at `/404.html` (`text/html; charset=utf-8`,
2540
+ `Cache-Control: no-store`); the runtime serves that page with status **404**
2541
+ for an unmatched `GET` or `HEAD`, with the same project security headers and
2542
+ `nosniff` as the built-in 404. Other methods keep the plain-text `Not found`.
2543
+ A route that matches but is disabled, mismatched by `match:` or denied by a
2544
+ policy keeps its own answer; only "no route matched" uses the page.
2545
+
2546
+ `/404.html` is also reachable directly and answers 200 (the same as on static
2547
+ hosting, which is why the path is fixed). It is one generated route in the
2548
+ `routes`/`audit` counts and `explain` reports it as `site.notFound`; the
2549
+ sitemap leaves it out. A route you declare at `/404.html` wins and is served
2550
+ as the not-found page instead. The page answers every unknown URL, so it cannot depend on the path requested.
2551
+
2552
+ `urlcode build --target static` writes it as the object `404.html` (see
2553
+ [static hosting](https://github.com/jimhoyd-com/urlcode/blob/main/docs/STATIC.md)); point the host's error document at that key.
2554
+ Cloudflare refuses it like any `page` route (no asset binding); the Worker
2555
+ has no per-request fallback page, so use the platform's own 404 asset there.
2556
+
2294
2557
  ### Per-target support
2295
2558
 
2296
- | Target | `robots`, `sitemap`, `securityTxt` (`respond`) | `favicon`, `llms` (`page`) |
2559
+ | Target | `robots`, `sitemap`, `securityTxt` (`respond`) | `favicon`, `llms`, `notFound` (`page`) |
2297
2560
  | --- | --- | --- |
2298
2561
  | self-hosted, Vercel, AWS | served | served |
2299
2562
  | Cloudflare | compiled into the artifact (`build --origin` for absolute URLs) | refused at build time like any `page` route: the target has no asset binding; serve them from the platform's static assets |
@@ -2532,6 +2795,10 @@ and management API. That package has since been retired and unpublished, so no
2532
2795
  supported stored-link extension ships today. Core no longer has a native `link`
2533
2796
  handler or a `dynamicLinks` project flag.
2534
2797
 
2798
+ The `store` extension is the data-owning counterpart: it serves declared,
2799
+ bounded collections as a CRUD API from an operator-owned directory. See
2800
+ [data store](https://github.com/jimhoyd-com/urlcode/blob/main/docs/STORE.md).
2801
+
2535
2802
  A project declares versioned configuration and exclusive route mounts:
2536
2803
 
2537
2804
  ```yaml
@@ -2746,7 +3013,11 @@ The UI extension exposes `transformView`, a synchronous filter called before a
2746
3013
  named kit template renders. It receives `{template, view}` and returns the view
2747
3014
  model to render. Use copy, templates, theme and CSS for ordinary presentation
2748
3015
  changes; use this hook for project-specific computed view data that those
2749
- declarative layers cannot express.
3016
+ declarative layers cannot express. It also exposes `transformPage`, called
3017
+ before the shared layout renders. It receives the editable title, layout,
3018
+ navigation, account menu and flash message and returns those page fields. This
3019
+ lets a product join auth/admin screens to its own shell without replacing their
3020
+ security or workflow behavior. Both filters are synchronous and trusted.
2750
3021
 
2751
3022
  ### Building an extension
2752
3023
 
@@ -2758,11 +3029,15 @@ An extension package should export a registration factory and, when it supports
2758
3029
  2. Publishes every project hook through `hooks` and reuses
2759
3030
  `extensionHooksSchema` plus `loadExtensionHooks`; it does not implement its
2760
3031
  own path resolver or dynamic-import cache.
2761
- 3. Activates all configuration, files, services and hooks before serving a
3032
+ 3. Publishes an `authoring` contract listing its supported project-owned
3033
+ configuration, theme/copy, component/template, stylesheet and hook surfaces,
3034
+ plus focused `fastChecks`. Keep descriptions concrete enough that an agent
3035
+ can choose a supported surface instead of copying package behavior.
3036
+ 4. Activates all configuration, files, services and hooks before serving a
2762
3037
  request. Invalid or stale configuration fails activation.
2763
- 4. Returns `handle` for mounts and optionally `authorize`/`middleware` for route
3038
+ 5. Returns `handle` for mounts and optionally `authorize`/`middleware` for route
2764
3039
  policies. It closes resources it owns.
2765
- 5. Keeps credentials, storage and provider setup in the operator host. Project
3040
+ 6. Keeps credentials, storage and provider setup in the operator host. Project
2766
3041
  YAML contains logical configuration and project-relative hook references.
2767
3042
 
2768
3043
  Consumers install the package, declare its YAML block and mounts/policies, and
@@ -2774,7 +3049,7 @@ See [Composing a site](https://github.com/jimhoyd-com/urlcode/blob/main/docs/COM
2774
3049
  ### Discovering schemas
2775
3050
 
2776
3051
  Each registration carries the JSON Schemas that validate its `config` block and
2777
- its per-route policy requirements, plus its hook contracts. `urlcode extensions` prints them together with
3052
+ its per-route policy requirements, plus its hook and authoring contracts. `urlcode extensions` prints them together with
2778
3053
  the project's own declarations so an author can see what a mount accepts:
2779
3054
 
2780
3055
  ```sh
@@ -2783,7 +3058,8 @@ urlcode extensions --project ./site --host-file /absolute/operator/host.mjs [--j
2783
3058
 
2784
3059
  For every registration in the host file it reports the name, contract version,
2785
3060
  targets, credential headers, configuration schema, policy schema (if any),
2786
- declared hook names, kinds, descriptions and input/output schemas,
3061
+ declared hook names, kinds, descriptions and input/output schemas, supported
3062
+ authoring surfaces and their fast checks,
2787
3063
  whether the project declares it, whether its `projectSha256` matches the current
2788
3064
  revision, the routes that mount it and the routes whose policies require it.
2789
3065
  Declared names the host does not register are listed as unregistered. The command
@@ -2856,7 +3132,27 @@ in memory (core zeroes `Uint8Array` contents after writing or on failure). The
2856
3132
  types are exported from `@jimhoyd/urlcode` for packages that want to typecheck
2857
3133
  against them.
2858
3134
 
2859
- Assembly rules, in `--with` order:
3135
+ `--with` is an unordered set. Core sorts the requested names before calling
3136
+ each `scaffold` (so `names` is the same for every spelling), then orders the
3137
+ results from the optional declarative fields on `ScaffoldResult`:
3138
+
3139
+ - `provides`: capability names the extension offers (for example `ui.kit`);
3140
+ a capability must not equal an extension name.
3141
+ - `requires`: extensions or capabilities that must be in the set and are
3142
+ placed before this extension. A missing one refuses, naming both.
3143
+ - `after`: the same ordering, without requiring presence.
3144
+ - `conflicts`: extensions or capabilities that must not be in the set.
3145
+ - Risky-scaffold acknowledgements are one generic channel, not a flag per risk. The operator repeats `--ack <extension>:<id>` (both parts lowercase letters, digits and hyphens, for example `store:public-write`); core validates the syntax, de-duplicates and sorts the values and hands them to every scaffold as the opaque `ScaffoldRequest.acknowledgements` (always present, possibly empty). An extension reads only ids qualified with its own name. To require one it throws an `Error` whose message states the risk and that carries `acknowledgement: '<name>:<id>'`; core appends the exact re-run command (the same `--with`, `--pin` and `--no-manifest`, plus every `--ack` already given and the new one), so an agent meets the acknowledgement only when it reaches that risk and never needs it in advance. A scaffold that used one lists it in `ScaffoldResult.acknowledged`; core refuses any `--ack` that no scaffold listed (a typo, an extension not in `--with`, or an unneeded value), before anything is written, and refuses a scaffold that lists an id it was not given or that is not its own. Acknowledgements are never written to project YAML; they are visible in command history, and the extension should state the resulting model in its generated README. `routeNotes` (single-line strings) are written as comments above that extension's routes; core renders them and infers no policy. Core adds no per-extension flag or result field: a new risk needs only a new id in the extension that owns it. `--allow-public-write` was a store-specific predecessor of this channel; it was never in a published core release and is removed rather than aliased (see [store](https://github.com/jimhoyd-com/urlcode/blob/main/docs/STORE.md)).
3146
+
3147
+ Core topologically orders by these, taking the lexically smallest ready
3148
+ extension first, so every permutation of the same set produces the same host,
3149
+ `urlcode.yaml` activation order and README. A cycle or a missing requirement or
3150
+ conflict refuses before anything is written, naming the extensions involved.
3151
+ Core never adds an extension (auth or ui) and never infers security policy
3152
+ from the set. Host setup should be self-contained (own identifiers, such as
3153
+ `storeProjectSha256`) unless it declares `requires` for what it references.
3154
+
3155
+ Assembly rules, in the resolved order:
2860
3156
 
2861
3157
  - Every package is resolved and every `scaffold` is called before anything is
2862
3158
  written. A name that is not installed refuses with the `npm install` command;
@@ -2870,7 +3166,7 @@ Assembly rules, in `--with` order:
2870
3166
  naming both sources.
2871
3167
  - `host.mjs` is all `hostImports`, then all `hostSetup` lines, then an
2872
3168
  `extensions` array of every `hostEntries` item, then `close()` running the
2873
- `hostClose` statements in reverse `--with` order so later entries release
3169
+ `hostClose` statements in reverse resolved order so later entries release
2874
3170
  before what they built on. Setup lines share one module scope: admin's entry
2875
3171
  references the `service`, `csrfKey` and `projectSha256` identifiers that
2876
3172
  auth's setup defines, which is why `names` carries the full list.
@@ -2973,8 +3269,10 @@ through the kit and refuse to activate without their own templates present.
2973
3269
 
2974
3270
  #### Supported combinations
2975
3271
 
2976
- `--with` order is the activation order, and the kit must be active before
2977
- anything that renders through it.
3272
+ `--with` is an unordered set. Core derives the activation order from each
3273
+ extension's declared requirements, so the kit is active before anything that
3274
+ renders through it, whatever order you name them in. A missing requirement
3275
+ (for example admin without auth) refuses before anything is written.
2978
3276
 
2979
3277
  | `--with` | Result |
2980
3278
  |---|---|
@@ -2984,6 +3282,8 @@ anything that renders through it.
2984
3282
  | `auth` or `auth,admin` | Refused: the scaffold names the missing `ui`. |
2985
3283
  | `auth,admin,ui` | Refused: `ui` must come before the extensions it renders. |
2986
3284
  | `admin` without `auth` | Refused: admin reuses auth's service, CSRF key and revision. |
3285
+ | `ui,auth,store` | Todo API and CRUD screen, both protected by `auth: true`. |
3286
+ | `store` or `ui,store` | Refused: the writable mount would be public. Add `auth`, or re-run the printed command with `--ack store:public-write` for a documented public-write scaffold; core rejects any `--ack` no scaffold consumed, such as one with auth composed or `store` absent. |
2987
3287
 
2988
3288
  Every refusal happens before anything is written, and leaves no directory
2989
3289
  behind. There is no auth-without-ui or admin-without-ui configuration in this
@@ -3054,12 +3354,20 @@ middleware, and the host-file trust boundary.
3054
3354
  The `ui` config's `copy`, `templates` and `stylesheet` paths point at the
3055
3355
  project's own directories. Nothing here forks a package.
3056
3356
 
3357
+ Treat the result as one application. Auth and admin keep ownership of sessions,
3358
+ CSRF, permissions, validation and mutations; the project owns its brand,
3359
+ product navigation and the smallest presentation differences it needs. Inspect
3360
+ `urlcode extensions --host-file ... --json` (MCP: `get_extensions`) and follow
3361
+ each registration's `authoring.surfaces` before copying package code.
3362
+
3057
3363
  | Override | File | Effect |
3058
3364
  |---|---|---|
3059
3365
  | Wording and translation | `ui/copy/<locale>.json` | Replaces catalogue ids, including ids the auth and admin packages own. Listed in `languages`. |
3060
3366
  | A whole screen | `ui/templates/<name>.html` | Shadows a kit or extension template of that name, for example `ui/templates/auth/sign-in.html` or `ui/templates/admin/dashboard.html`. |
3061
3367
  | Styling | `ui/extra.css` | Appended after the kit stylesheet; `{file, replace: true}` replaces it instead. |
3062
3368
  | Colours, logo, favicon, radius, font | the `theme` block | Declarative; no file needed. |
3369
+ | Computed view data | `extensions.ui.config.hooks.transformView` | Adds project data before a named template renders. |
3370
+ | Product shell and navigation | `extensions.ui.config.hooks.transformPage` | Changes title, layout, navigation, account menu or flash before the shared layout renders. |
3063
3371
 
3064
3372
  A template is data in the kit's own language. It cannot add a script, change
3065
3373
  what a form validates, or change what a page sends in headers — so an override
@@ -3105,6 +3413,13 @@ not run it. With the packages named:
3105
3413
  generated README with the flag already set. `@jimhoyd/urlcode-ui` depends on
3106
3414
  neither peer; the operator names them.
3107
3415
 
3416
+ Run the extension's published `fastChecks` while editing. Theme and copy changes
3417
+ need no framework build. Template and CSS checks load only the UI kit and named
3418
+ namespaces; the full runtime validation and request suite remain the final
3419
+ evidence. A React product frontend with `components.json` should also install
3420
+ and follow the official shadcn/ui skill. The server template kit uses compatible
3421
+ tokens but does not accept React components.
3422
+
3108
3423
  Overrides of extension templates and of extension-owned catalogue ids reach
3109
3424
  the rendered screens, which is what the regression test below asserts.
3110
3425