@jimhoyd/urlcode 0.4.2 → 0.4.6

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 +19 -15
  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 +43 -5
  86. package/package.json +15 -22
  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
@@ -1,222 +0,0 @@
1
- # Observability
2
-
3
- URLCode reports what it does through three interfaces built on one stream of
4
- events: the JSON log on stdout, operator **observers** that receive the same
5
- events in process, and a **metrics snapshot** of counters derived from them,
6
- optionally served in Prometheus format. Observers mirror [host plugins](PLUGINS.md):
7
- they are JavaScript an operator passes to `startServer` or `createRuntime`,
8
- never something a project's YAML can name, and they run with the host's
9
- privileges. [Monitoring](MONITORING.md) is the operator's guide to probes,
10
- recipes and alerts; this page is the contract.
11
-
12
- ```js
13
- import { startServer } from '@jimhoyd/urlcode';
14
-
15
- await startServer({
16
- project: './site',
17
- observers: [myObserver], // in-process event and metrics sinks
18
- metrics: true, // GET /_urlcode/metrics, Prometheus text format
19
- });
20
- ```
21
-
22
- ## Event catalogue
23
-
24
- `events` in `@jimhoyd/urlcode/observability` is a frozen object mapping each event name
25
- to the complete list of fields it may carry. A test runs a real server and
26
- holds every record to it, so a field or event that is not in the table below
27
- does not ship. Fields marked *optional* are present only in the situations
28
- named.
29
-
30
- | Event | Fields | Emitted when |
31
- |---|---|---|
32
- | `request` | `requestId` string, `status` integer, `durationMs` number; `method` string and `route` string or `null` with `--request-log detailed` | Every response the server wrote, including probes and shed 503s. `route` is the configured pattern (`/u/{id}`) or the probe path, never the requested path. |
33
- | `reload` | `status` `ok`/`rejected`; `version` string and `routes` integer on `ok` | `app.reload()` or the development watcher swapped, or refused to swap, the snapshot. |
34
- | `watch` | `status` `failed` | The development watcher could not fingerprint the project. |
35
- | `function_worker` | `status` `started`/`restarting`, `slot` integer; `attempt` and `delayMs` integers on `restarting` | A function worker became ready or is scheduled for replacement. |
36
- | `signal` | `outcome` (`accepted`, `delivered`, `failed`, `dropped`), positive `count` | Best-effort webhook totals; no destination, request data or secrets. |
37
- | `logs_dropped` | `count` integer | The JSON logger shed records because stdout was not writable. Written by the logger itself, so observers do not see it. |
38
- | `observer` | `status` `failed`, `name` string | An observer hook threw or rejected. Written to the default log only, never to observers. |
39
- | `throttle` | `route`, `outcome` `allowed`/`exceeded`, `remaining` integer | A throttle decision. `allowed` is logged only in `mode: report`; enforce mode logs refusals. |
40
- | `agents` | `route`, `list` string, `outcome` `denied`/`reported` | A User-Agent matched a list. The list name is logged, never the header. |
41
- | `cache` | `route`, `outcome` `hit`/`stale`/`miss`/`store` | A cache lookup or store. |
42
- | `listening` | `address`, `port`, `mode`, `origin` | Printed once by the CLI at startup, not emitted by the server. |
43
-
44
- Every event carries `event` (its name). Numbers are JSON numbers, never
45
- strings.
46
-
47
- ### Privacy guarantees
48
-
49
- No event, snapshot or exposition carries a request URL, path, query string,
50
- header, body, client address, User-Agent string, binding, secret or user
51
- exception text. `route` is always a configured pattern
52
- from reviewed YAML. `requestId` is server-generated unless
53
- `--trust-request-id` accepts one from a trusted proxy. An observer
54
- that logs should keep the same rule; nothing in an event lets it break it.
55
-
56
- ## Observers
57
-
58
- ```js
59
- const myObserver = {
60
- name: 'forwarder', // ^[a-z][a-z0-9-]{0,63}$, unique per server
61
- version: '1.0.0', // any string up to 64 characters
62
- onEvent(event) {}, // every record the JSON logger writes, in order
63
- onMetrics(snapshot) {}, // the metrics snapshot, on the interval and at close
64
- async onClose() {}, // release resources; reverse order
65
- };
66
- ```
67
-
68
- In TypeScript the contract is `Observer` from `@jimhoyd/urlcode/observability` (also
69
- exported from `urlcode`), with `ObserverEvent` for a record and
70
- `MetricsSnapshot` for what `onMetrics` receives; the declarations ship with the
71
- package:
72
-
73
- ```ts
74
- import type { Observer, ObserverEvent, MetricsSnapshot } from '@jimhoyd/urlcode/observability';
75
-
76
- const myObserver: Observer = {
77
- name: 'forwarder',
78
- version: '1.0.0',
79
- onEvent(event: ObserverEvent) { queue.push(event); },
80
- onMetrics(snapshot: MetricsSnapshot) { gauge.set(snapshot.requests.inFlight); },
81
- };
82
- ```
83
-
84
- Validation (`validateObservers`) matches plugins: at most 32 observers, each
85
- an object with a kebab-case `name` no other observer uses, a `version` string,
86
- every declared hook a function and at least one present. It runs before the
87
- listener starts, so a bad observer fails startup rather than a request.
88
-
89
- `onEvent` receives the same object the logger serialised, after the logger,
90
- observers in array order. Do not mutate it. It runs on the request path, so
91
- keep it cheap: buffer and flush on a timer rather than awaiting a network
92
- call. A hook that throws or returns a rejecting promise is isolated: the
93
- request is unaffected, the next observer still runs, `observers.errors` in
94
- the snapshot increments and one `observer` record goes to the default log.
95
- Nothing is retried; an observer that needs delivery guarantees owns its own
96
- queue.
97
-
98
- `onMetrics` receives a fresh snapshot every `metricsIntervalMs`
99
- (`startServer` option, `0` off by default, 1 s to 1 h) and once at `close()`.
100
- `onClose` runs in reverse order after the runtime has closed.
101
- `app.observers` lists the `{ name, version }` pairs.
102
-
103
- `createRuntime(project, { observers })` takes the same array for embedding
104
- without the server: the runtime's own sink and counters are then yours, and
105
- `runtime.metrics()` returns its snapshot. `startServer` never passes its
106
- observers down to the runtimes it creates, so a reload does not re-register
107
- them and counters survive reloads.
108
-
109
- `createObserverSink(observers, fallbackLog)` is the fan-out itself, exported
110
- for tests and custom hosts: it returns a `log(event)` function with `.metrics`,
111
- `.publish(snapshot)` and `.close()`.
112
-
113
- ## Metrics snapshot
114
-
115
- `app.metrics()` and `runtime.metrics()` return a plain object, safe to
116
- `JSON.stringify`, of counters since the process started serving. Numbers
117
- only; the one keyed table is `requests.byRoute`, keyed by configured pattern
118
- and capped at 10 000 keys.
119
-
120
- | Field | Type | Meaning |
121
- |---|---|---|
122
- | `version` | gauge | Snapshot shape version, currently `2`. |
123
- | `uptimeSeconds`, `rssBytes` | gauge | Process facts. |
124
- | `requests.total`, `requests.byStatusClass.{2xx,3xx,4xx,5xx}` | counter | Application responses, including shed 503s. |
125
- | `requests.inFlight` | gauge | Requests holding application admission now. |
126
- | `requests.byRoute[pattern]` | counter | Responses per matched route. A shed or unmatched request has no route. |
127
- | `health.total`, `health.byStatusClass`, `health.inFlight` | counter, gauge | The probe budget: `/_urlcode/health`, `/_urlcode/ready` and `/_urlcode/metrics`. |
128
- | `shed.requests`, `shed.health` | counter | 503s answered because an admission budget was full. |
129
- | `reloads.ok`, `reloads.rejected` | counter | Snapshot swaps. |
130
- | `watch.failed` | counter | Development watcher failures. |
131
- | `functionWorkers.started`, `functionWorkers.restarts` | counter | Worker starts and scheduled replacements. |
132
- | `functionWorkers.healthySlots`, `functionWorkers.slots` | gauge | Ready slots and configured slots of the serving runtime. |
133
- | `policies.throttle.{allowed,exceeded}` | counter | Throttle decisions (see the catalogue for what enforce mode logs). |
134
- | `policies.agents.{denied,reported}` | counter | Agents decisions. |
135
- | `policies.cache.{hit,stale,miss,store}` | counter | Cache outcomes. |
136
- | `signals.{accepted,delivered,failed,dropped}` | counter | Best-effort webhook outcomes; exposed as `signals_total` with outcome labels. |
137
- | `logsDropped` | counter | Records the JSON logger shed. |
138
- | `observers.errors` | counter | Observer hooks that threw or rejected. |
139
-
140
- Policy counters are derived from the `throttle`, `agents` and `cache` events
141
- as they pass through the sink, so the policies themselves have no metrics
142
- code. Runtime facts that never become events (admission, shedding, slot
143
- health) are recorded by the server directly. Counters are per process;
144
- aggregation across replicas is the scraper's job.
145
-
146
- ## Prometheus exposition
147
-
148
- `startServer({ metrics: true })`, or `urlcode serve --metrics` on the command
149
- line, serves `GET /_urlcode/metrics` as `text/plain; version=0.0.4`, rendered from the same snapshot by
150
- `renderPrometheus(snapshot)`, a pure function you can also call yourself.
151
- Every metric is prefixed `urlcode_`; counters end in `_total`; the only labels
152
- are `status_class`, `route` and `outcome`.
153
-
154
- ```
155
- # HELP urlcode_requests_total Application requests answered since start, by status class.
156
- # TYPE urlcode_requests_total counter
157
- urlcode_requests_total{status_class="2xx"} 1042
158
- urlcode_route_requests_total{route="/u/{id}"} 977
159
- urlcode_requests_in_flight 3
160
- urlcode_shed_total{outcome="requests"} 0
161
- urlcode_reloads_total{outcome="ok"} 2
162
- urlcode_function_worker_restarts_total 0
163
- urlcode_function_worker_healthy_slots 2
164
- urlcode_throttle_total{outcome="exceeded"} 14
165
- urlcode_cache_total{outcome="hit"} 511
166
- urlcode_logs_dropped_total 0
167
- urlcode_observer_errors_total 0
168
- urlcode_uptime_seconds 86400
169
- urlcode_process_rss_bytes 71303168
170
- ```
171
-
172
- The endpoint is **off by default**. It shares the probes' admission budget
173
- (`--max-in-flight-health`) and the same bind host, which is `127.0.0.1`
174
- unless `--host` says otherwise. Like the probes it is unauthenticated and
175
- discloses route patterns and traffic shape, so **do not expose it publicly**:
176
- keep it on an internal interface or restrict it at the ingress. A scrape
177
- counts under `health`, not under application requests. See
178
- [`examples/monitoring/prometheus-scrape.yaml`](../examples/monitoring/prometheus-scrape.yaml).
179
-
180
- ## OpenTelemetry sketch
181
-
182
- The runtime has no OpenTelemetry dependency. An observer can forward events
183
- to an OTLP exporter and map the snapshot onto instruments; this is a sketch,
184
- not shipped code, and omits batching, resource attributes and error handling.
185
-
186
- ```js
187
- // Sketch. `logs` and `meter` come from the OpenTelemetry SDK the operator
188
- // configures; the runtime knows nothing about them.
189
- export function otelObserver({ logger, meter }) {
190
- const requests = meter.createCounter('urlcode.requests', { unit: '{request}' });
191
- const inFlight = meter.createObservableGauge('urlcode.requests.in_flight');
192
- let last;
193
- inFlight.addCallback(result => { if (last) result.observe(last.requests.inFlight); });
194
- return {
195
- name: 'otel', version: '0.1.0',
196
- onEvent(event) {
197
- // Every field is already safe to attach as an attribute.
198
- logger.emit({ body: event.event, attributes: event });
199
- if (event.event === 'request') requests.add(1, { status_class: `${Math.floor(event.status / 100)}xx`, route: event.route ?? '' });
200
- },
201
- onMetrics(snapshot) { last = snapshot; }, // the gauge reads the latest snapshot
202
- };
203
- }
204
- ```
205
-
206
- Counters in the snapshot are cumulative, so they map to OpenTelemetry
207
- `Counter` instruments read through an observable callback, or to a
208
- Prometheus receiver scraping `/_urlcode/metrics` directly. `durationMs` on
209
- `request` is the input for a `Histogram`; the runtime does not bucket it.
210
-
211
- ## What is not provided
212
-
213
- - **Tracing.** There are no spans and no context propagation; `requestId` is
214
- the only correlation key, and a trusted proxy can supply it.
215
- - **Sampling.** Every event is delivered to every observer, or shed by the
216
- logger under back-pressure and reported as `logs_dropped`.
217
- - **Persistence and aggregation.** Counters live in process memory, reset on
218
- restart, and describe one process. Retention and cross-replica sums belong
219
- to the collector.
220
- - **Per-URL analytics.** By design; see the privacy guarantees.
221
- - **Adapters.** Vercel, Lambda and Cloudflare handlers emit through the
222
- platform's own logging and do not take observers.
@@ -1,149 +0,0 @@
1
- # Decisions to align
2
-
3
- Last reconciled 2026-09-20 against open and closed issues (#242); the source
4
- review it began from is dated 2026-09-19 (core `db375bf`, now archived).
5
- This is the maintainer's decision list, not a second implementation backlog.
6
- The [roadmap](../ROADMAP.md) gives sequence and the [archive](archive/README.md)
7
- keeps earlier discussions. Recommendations below are not accepted decisions.
8
-
9
- ## Principles already settled, in plain language
10
-
11
- - **Describe first, code only when needed.** Use a supported YAML feature or
12
- extension before writing plumbing. Custom application code is still welcome.
13
- - **Your application code runs like normal Node code.** Functions and middleware
14
- are trusted by default. `sandbox: true` deliberately restricts a route's whole
15
- function/middleware chain. Request data still needs validation in either mode.
16
- - **A grant controls what URLCode supplies, not what trusted code can access.**
17
- Trusted code can independently read the host environment, files and network.
18
- Opt-in sandboxing retains its existing isolation and revision-pinned grants.
19
- - **Portable does not mean every host supports every feature.** Keep infrastructure
20
- out of route YAML and reject unsupported targets before activation.
21
- - **Core works alone; optional packages add accounts, admin and presentation.**
22
- Core never imports those implementations. Shared UI belongs in `urlcode-ui`.
23
- - **Documentation stays beside the owning code.** Core guides live here;
24
- extension contracts and implementation status live in their repositories.
25
- `urlcode-docs` is deleted. `urlcode-short` and `urlcode-dynamic-link` are retired.
26
- - **Passing tests proves the tested behavior.** It does not prove deployment,
27
- accessibility, hostile tenant isolation or independent security assessment.
28
- - **Keep the free runtime useful.** Apache-2.0 remains unchanged; no mandatory
29
- hosted account, paid capability gate or provider lock-in belongs in core.
30
-
31
- ## Decisions still needed
32
-
33
- | Decision | What the code says today | Recommendation and consequence |
34
- |---|---|---|
35
- | Where does work status live? | Several old plans repeated issues and continued calling delivered work unfinished. | Issues for actionable status, this short roadmap for sequence, archive for completed proposals. Preserve evidence gaps when archiving. |
36
- | Expand into business applications now? | No collection handler or proposed business suite is implemented; the model-backed benchmark evidence is missing. | Measure existing tasks and record repeated application plumbing before selecting a collection/CMS/forms project. Retired short-link products stay retired. [Proposal](SPIKE-BUSINESS-SUITE.md). |
37
- | Fold extension schemas into retrieved context? ([#174](https://github.com/jimhoyd-com/urlcode/issues/174), open) | `urlcode extensions` and the MCP `get_extensions` query return the registered configuration and policy schemas, but `src/context.ts:113` reports `extensions` as names only, so an author writing `extensions.<name>.config` or `policies.extensions.<name>` must run the separate operator-authorized query first. | Decide from retrieval and task evidence, not preference: the existing small-task harness can measure whether folding schemas into bounded context improves authoring. Keep the token budget bounded and never auto-load a project-selected host file. This is a discovery improvement, not a defect in the existing query. |
38
- | Build the tested-image promotion path? ([#233](https://github.com/jimhoyd-com/urlcode/issues/233), open) | [Design and an inert invariant helper](CONTAINER-PROMOTION.md) are merged. `release.yml` still rebuilds from source when `PUBLISH_CONTAINER` is true, and a retry accepts an existing `:VERSION` on its `revision` label alone. Live GHCR state and whether GHCR preserves an OCI digest through `skopeo`/`crane` are unverified. | The reason is integrity (tested bytes are the shipped bytes), not speed: CI's image build took about 12 seconds, so a cross-run build cache is not justified and should not be added. Recommendation: do not implement while publication is off and GHCR is uninspected. First grant `read:packages` and inspect historical labels and digests; then land a candidate-side image build behind an operator input, inspect one real candidate, and only then change the publisher. Not changed here. |
39
- | Keep the POST-plus-`request.body` sandbox advisory? | `src/readiness.ts` nudges any code-running route that accepts POST with a declared `request.body` and declares neither `sandbox: true` nor `sandboxReason`. It is advisory only: never fails `audit`, never changes `ready`. | The nudge keys on request *shape* while [AI authoring](AI-AUTHORING.md) tells authors to decide on *code* trust, so it can read as "untrusted input implies sandbox" -- the reasoning that guidance explicitly rejects. It still has value as a prompt to record a decision. Recommendation: keep the trigger, restate the message as a request to record the trust decision (`sandbox: true` or `sandboxReason`) rather than as a suggestion that this route may need isolation. Not changed here; #196 was a docs/tooling alignment pass. |
40
-
41
- The broader [AI benchmark proposal](SPIKE-AI-FRAMEWORK-BENCHMARK.md) also needs a
42
- chosen application, model-run budget and execution authorization. The existing
43
- small-task harness can supply evidence without committing to that larger study.
44
-
45
- ## Accepted: one Node deployment per project
46
-
47
- **Decided 2026-09-19.** Projects that use `function` or `middleware` deploy as
48
- **one trusted Node process** — a container or a VM running the project as it
49
- runs locally. That is the supported execution model, and it needs no new work:
50
- it is what the runtime already does.
51
-
52
- **Per-route Lambda compilation is not pursued.** The alternative on the table
53
- was a build step emitting one Lambda per `function` route
54
- ([the proposal](archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md)). It is declined for now, on three
55
- grounds the proposal itself states:
56
-
57
- 1. It would replace the sandbox guarantee rather than preserve it, and lose the
58
- fresh-per-invocation state that `sandbox: true` currently guarantees.
59
- 2. It would make this project the author of generated IAM roles — a
60
- security-critical output it has never owned.
61
- 3. It would trade an honest refusal for a larger claim nobody has deployed.
62
-
63
- Against that, a single Node deployment supports every route type today with no
64
- compiler, no generated infrastructure and no second isolation story to document.
65
-
66
- **What follows from this decision:**
67
-
68
- - AWS and Vercel continue to refuse `function` and `middleware` at activation,
69
- naming the route (`src/capabilities.ts`, `activateNativeOnly` in
70
- `src/adapters.ts`). That refusal is now a **deliberate position**, not a gap
71
- awaiting an adapter. Documentation should say so rather than implying the
72
- support is coming.
73
- - Serverless targets remain first-class for the declarative route types they can
74
- actually serve; nothing about static or native-only deployment changes.
75
- - [SPIKE-LAMBDA-COMPILE.md](archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md) is kept as the analysis
76
- behind this decision, not as a plan. Reopen it only on evidence of real demand
77
- for URLCode `function` routes specifically on AWS serverless — the proposal's
78
- own §6 already scopes what a first attempt would be.
79
-
80
- This decision is about the *execution model*, not about AWS. Deploying the Node
81
- process to AWS (ECS, EC2, App Runner) is an operator choice this fully supports.
82
-
83
- ## Accepted: per-package release tags
84
-
85
- **Decided 2026-09-19.** Workspace packages under `packages/` release on
86
- Changesets' own `<package name>@<version>` form — for example
87
- `@jimhoyd/urlcode-ui@0.1.0-alpha.6`. Core keeps bare `v*`.
88
-
89
- **The problem.** Core and all three extensions arrived here triggering on
90
- `tags: ['v*']`, and their alpha tags overlap outright: ui shipped
91
- `v0.1.0-alpha.2` through `-alpha.5`, admin `v0.1.0-alpha.1` and `-alpha.3`,
92
- auth `v0.1.0-alpha.1` through `-alpha.3`. Across four repositories that was
93
- fine. In one repository a single bare tag push starts more than one release
94
- workflow. Each one fails closed on its own tag-matches-manifest check, so
95
- nothing can mis-publish — but "two workflows race and one errors on every
96
- release" is not a release process, and the failure is confusing rather than
97
- informative.
98
-
99
- **Why Changesets' form rather than a prefix like `ui-v0.1.0-alpha.6`.** Both
100
- work and both are valid ref names. The deciding factor is that Changesets is
101
- already the chosen release flow, and `changeset tag` emits the
102
- `<name>@<version>` form natively. Picking anything else means writing and
103
- maintaining a translation layer between the tool that computes the version and
104
- the tag that triggers the publish — new code whose only job is to disagree
105
- with a default. The spike chose Changesets partly because it is "cheap and
106
- low-risk for an agent or a human to generate correctly"; hand-rolling the tag
107
- shape undercuts exactly that.
108
-
109
- **Why the two schemes cannot collide.** A scoped package name begins with `@`,
110
- and GitHub's `v*` filter requires a leading `v`, so no tag can match both.
111
- Verified rather than assumed, including that `*` does not match `/` in a filter
112
- pattern, so `@jimhoyd/urlcode-ui@*` matches the version segment only.
113
-
114
- **Core's asymmetry is forced, not preferred.** Under layout A core is the
115
- repository root rather than a workspace member, so Changesets does not manage
116
- it and `changeset version` will not bump it. Core therefore keeps the tag
117
- scheme and release workflow it already had.
118
-
119
- [`scripts/check-release-tags.ts`](../scripts/check-release-tags.ts) enforces
120
- this in `npm run check`: it rejects a workspace package workflow that does not
121
- trigger on its own `<name>@*`, rejects any workflow other than core's claiming
122
- `v*`, and independently asserts that no two filters can match the same tag. The
123
- reasoning above is the kind of prose that rots as soon as `auth` and `admin`
124
- arrive, which is the whole argument this repository makes for enforcing checks
125
- over documented intent.
126
-
127
- ## Completed work (archived)
128
-
129
- Closed questions, the completed monorepo migration (ui, auth and admin are
130
- workspace packages; released from this repository) and the 2026-09-19 source
131
- review baseline live in
132
- [archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md](archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md).
133
- The migration plan is [archived](archive/2026-09-19/SPIKE-MONOREPO.md). Versions
134
- and channels are in [version alignment](VERSION-ALIGNMENT.md) and
135
- `npm run release:status`, not here.
136
-
137
- ## Accepted: middleware withdrawn rather than consolidated
138
-
139
- **Decided 2026-09-19.** `@jimhoyd/urlcode-middleware` was unpublished and its
140
- repository deleted; there is no `packages/middleware` and nothing to fold into
141
- core afterward (#172, closed as moot). Per-route middleware is native to core
142
- via the `middleware:` array ([MIDDLEWARE.md](MIDDLEWARE.md)). The generic
143
- extension wrapping hook (`ExtensionInstance.middleware`,
144
- `RuntimeExtension.cacheSensitive`) stays in core's contract for other extensions
145
- but is no longer exercised by any shipped package, so do not assume it is
146
- covered. Static targets keep rejecting request-time middleware. The earlier
147
- instruction to move middleware into the monorepo first was overtaken; its full
148
- text is retained in the archived record.
149
-
@@ -1,41 +0,0 @@
1
- # Operational evidence and deployment acceptance
2
-
3
- `node scripts/operational-drills.ts` creates and deletes its own temporary
4
- project. It runs mixed real HTTP requests against native redirects and
5
- `function` routes; asserts responses; rejects a bad reload; and activates and
6
- rolls back a configuration. `URLCODE_SOAK_SECONDS=60` selects a longer run
7
- (1–3600 seconds, default 5). Output is JSON with request count, batch p99
8
- duration and RSS. Batch latency is not per-request p99 or a capacity promise.
9
- CI's `verify` job runs the short drill across a 3-Node (22/24/26) × 3-OS
10
- (Linux/macOS/Windows) matrix, nine combinations, but only on a push to `main`.
11
- A pull request runs the same drill across all three Node versions on Linux
12
- only (3 of the 9 combinations); the macOS/Windows legs only run once a PR
13
- merges, per `.github/workflows/ci.yml`'s matrix.
14
-
15
- Core has no durable store of its own, so this drill has no backup/restore or
16
- disk-exhaustion exercise: an extension package owning durable state is
17
- responsible for its own persistence proof. No such package ships today — the
18
- `urlcode-dynamic-link` package that did has been retired.
19
-
20
- ## Required proof on the intended deployment
21
-
22
- Local/CI passes do not close these gates. `urlcode verify-deployment --target`
23
- ([deployment checks](DEPLOYMENT-CHECKS.md)) records that the deployed responses
24
- match the project and is the first step of the rollback drill below, not a
25
- substitute for it. The deployment owner must record:
26
-
27
- | Exercise | Acceptance evidence |
28
- |---|---|
29
- | Soak | At least 24 hours at expected peak and burst load through actual TLS/ingress; native/function mix, slow clients, response correctness, p50/p95/p99, throughput, error/rejection rate, RSS plateau, CPU and FD growth. Define numerical SLOs before starting |
30
- | Kill/restart | Kill the server with outstanding requests. Reconcile uncertain mutations by request ID, restore readiness, and prove no duplicate side effects from application code |
31
- | Rollback | Deploy candidate by exact digest beside last-good, run route assertions, switch ingress, drain, then switch back. Verify configuration/policy compatibility |
32
- | Monitoring | Deliver test alerts for missing logs, sustained errors, pool rejection/failure, low disk, restart storms and failed readiness to a named on-call owner |
33
-
34
- If your deployment adds a durable-state extension, add that extension's own
35
- backup/restore, logical export/import and disk/log exhaustion exercises to
36
- this table; core's proof above does not cover them.
37
-
38
- Record date, operator, source/app/policy/image digests, topology, hardware/limits,
39
- commands, duration, synthetic dataset size, raw metrics/log locations, result
40
- and unresolved findings. A reviewer signs the acceptance record; a blank
41
- record is not a pass.
@@ -1,201 +0,0 @@
1
- # Running URLCode yourself
2
-
3
- This is the stable 0.1 self-hosted runtime. Its deliberately bounded feature set
4
- is not a claim of suitability for every production workload. Deploy only workloads
5
- whose requirements fit the [implemented contract](SPECIFICATION.md).
6
- Provider adapters, automatic TLS/DNS management, distributed rate limits,
7
- metrics exporters and durable event delivery are not included.
8
-
9
- ## Process deployment
10
-
11
- Install a reviewed URLCode commit with Node 22.13+ and `npm ci --omit=dev`.
12
- Keep the runtime separate from an application checkout pinned to its own commit.
13
- Functions support only relative project JavaScript modules; do not install or
14
- execute an untrusted application’s package scripts as part of serving it. Validate using
15
- the same injected environment as the serving process:
16
-
17
- ```sh
18
- node /opt/urlcode/dist/cli.js validate --project /srv/my-links
19
- node /opt/urlcode/dist/cli.js serve --project /srv/my-links \
20
- --host 127.0.0.1 --port 3000 --origin https://links.example.com
21
- ```
22
-
23
- `--origin` defines the public URL seen by functions; proxy Host/X-Forwarded-*
24
- headers are intentionally not trusted. Use a process supervisor that restarts on
25
- failure and sends SIGTERM for shutdown. Shutdown stops accepting requests, gives
26
- HTTP connections up to 10 seconds, and drains bounded in-flight functions.
27
-
28
- Serve a read-only application tree where practical. The operator-owned runtime
29
- account must be able to read application files/dependencies. Authoring happens
30
- in development/CI, not by modifying a running replica's filesystem.
31
-
32
- ## Container deployment
33
-
34
- The supplied image packages the runtime; it does not copy your application or
35
- local secret files. Build from the reviewed runtime checkout:
36
-
37
- ```sh
38
- docker build -t urlcode:0.3.0 .
39
- docker run --rm --name my-links \
40
- --read-only --cap-drop ALL --security-opt no-new-privileges \
41
- --memory 512m --cpus 1 --pids-limit 128 \
42
- -p 127.0.0.1:3000:3000 \
43
- -v "$PWD/starters/default:/project:ro" \
44
- urlcode:0.3.0
45
- ```
46
-
47
- Replace the example mount with your app. The image uses the unprivileged `node`
48
- user; ensure mounted config/functions are readable by it. Core has no writable
49
- mount of its own; a future mount-based extension (like `auth`/`admin`, see
50
- [extensions](EXTENSIONS.md)) is the place for operator-owned writable state.
51
- Sandboxed application functions cannot access mounted files or installed
52
- Node packages. The resource values above illustrate
53
- container limits, not a sizing recommendation; large configuration compilation
54
- can need more memory. Measure your workload. Tag/redeploy immutable image digests
55
- in real operation rather than treating a mutable tag as a rollback identity.
56
-
57
- ## Domains, HTTPS and exposure
58
-
59
- Point your domain's DNS at the reverse proxy/load balancer you operate, terminate
60
- HTTPS there, and forward to the loopback/private URLCode port. Use a tested proxy
61
- such as your existing Caddy/nginx/load-balancer setup for certificates, connection
62
- limits and rate limiting. No certificate automation is supplied by URLCode yet.
63
- Keep direct backend access private. Restrict `/_urlcode/*` endpoints to operators
64
- at the proxy; they are unauthenticated and reveal route count/config digest.
65
-
66
- If functions perform sensitive actions, implement authentication and authorization
67
- in the application. A short URL is not automatically an access-control mechanism.
68
- Functions and middleware run trusted and unsandboxed by default, in the host
69
- process with full Node, filesystem and network access; a route that declares
70
- `sandbox: true` runs isolated in QuickJS/WebAssembly instead (see
71
- [function security](FUNCTION-SECURITY.md)). Keep separate deployment
72
- processes/containers and narrowly scoped credentials as additional boundaries.
73
- Do not expose a public code-upload/multi-tenant service on the basis of the self-hosted release alone
74
- without separate security review and stronger service-level containment.
75
-
76
- ## Secrets and rotation
77
-
78
- `dev`, `test`, `routes`, `audit`, `benchmark` and `validate --local` read
79
- `.env.local`. Authoring and
80
- permissions inspection do not read credentials or execute functions. `serve` and
81
- ordinary `validate` use process environment only. Resolve logical names from
82
- your own secret store/supervisor and inject them at startup; direct provider
83
- secret-store integrations remain future work. Bindings also require an external,
84
- revision-pinned operator policy; see [setup](FUNCTION-SECURITY.md). Do not place secret values in
85
- command-line arguments, route YAML, image layers or Git.
86
-
87
- Check tracked files as well as ignore rules. Docker builds use an explicit
88
- allowlist; npm artifacts include runtime/schema/starter/docs files only. Do not
89
- build an application image by blindly copying its entire development directory.
90
- Rotate a credential by replacing its injected value and restarting/redeploying;
91
- production does not watch or refresh secret values automatically.
92
-
93
- ## Health, logs and limits
94
-
95
- - `GET /_urlcode/health`: process liveness.
96
- - `GET /_urlcode/ready`: 200 when the active snapshot and all function workers
97
- are available; 503 while a worker
98
- is unavailable. Busy workers alone do not
99
- mark readiness down. A failed worker is replaced with
100
- exponential backoff (250 ms doubling to a 30-second ceiling) and readiness
101
- reports 503 until every slot is serving again. Replacement does not stop, so a
102
- request-triggered deadline cannot disable functions until an operator restarts;
103
- a cause that keeps recurring keeps the instance shedding load and needs an
104
- operator. Alert on sustained `function_worker` restart events.
105
- - Probes are answered from their own admission budget (16 by default,
106
- `--max-in-flight-health`), so they stay available while the application is
107
- saturated without being an unmetered endpoint. They are unauthenticated and
108
- report the configuration digest and route count: keep them on an internal
109
- interface or restrict them at the ingress.
110
- - Request logs: JSON request ID, status and duration. No URLs, query strings,
111
- headers, bodies, bindings or user exception text. `--request-log detailed` adds
112
- the request method and the matched route pattern (`/u/{id}`, or `null` when
113
- nothing matched). Both come from the reviewed configuration, never from
114
- request-supplied path, parameter or query text, which is what makes per-route
115
- error rates and latency available without logging user data.
116
- - Request IDs are generated per request and returned in `x-request-id`. An
117
- inbound `x-request-id` is ignored unless `--trust-request-id` is set, which is
118
- only correct when a trusted proxy sets the header and strips client-supplied
119
- copies; untrusted values are still rejected unless they are a single header of
120
- at most 128 characters from `[A-Za-z0-9_.:-]`. Forward stdout to your log
121
- system and alert on sustained 5xx and latency. The default logger drops records
122
- when stdout buffering reaches 1 MiB and reports the dropped count when output
123
- recovers; alert on `logs_dropped`. Function console output is
124
- suppressed; app-specific diagnostics are not yet a first-class feature.
125
- Synchronous and asynchronous sink failures are contained; a failed sink drops
126
- subsequent output and needs operator recovery. Collectors own rotation/retention.
127
- - HTTP: 8,192-character target, 16 KiB headers, 1 MiB buffered body, 15-second request
128
- receipt timeout, 10-second header timeout, 5-second keep-alive, 1,000 requests
129
- per socket and 1,024 active connections. At most 64 application requests are
130
- admitted through response completion; excess requests receive 503. Health probes
131
- remain available under admission saturation. A 15-second socket inactivity
132
- timeout closes stalled readers/writers. Proxy timeouts/rate limits still matter.
133
- - Functions: a `sandbox: true` route gets 2 concurrent workers (`--workers`),
134
- no queue and a 5-second deadline (`--function-timeout-ms`); a trusted route
135
- (`sandbox` false or absent, the default) shares the in-flight admission cap
136
- instead of a worker pool and races the same deadline. Either mode buffers
137
- 1 MiB of response (`--max-response-bytes`) and 16 KiB response headers.
138
- Saturation 503; timeout 504; error 502.
139
- QuickJS guests have a 32 MiB heap and 512 KiB stack budget and no network or
140
- host capabilities; a trusted route has neither budget and full Node access.
141
- Outer workers have additional V8 limits. Total process/WASM
142
- memory still needs deployment-level limits; do not equate guest budget with RSS.
143
-
144
- `urlcode serve`/`dev` and the JavaScript server API both configure workers,
145
- deadlines and byte limits: `--workers`, `--function-timeout-ms`,
146
- `--max-response-bytes`, `--max-body-bytes`, `--max-in-flight` and
147
- `--max-in-flight-health`. Set them on the container command line; these are
148
- deployment controls, not portable route behavior. Horizontal replicas
149
- must use identical application/config versions and secret bindings. In-memory
150
- function state is reset after every invocation, not durable/shared application state.
151
- General application storage needs a future explicit capability broker; no
152
- storage/network access is exposed to the guest. Core no longer has a native
153
- link store, and the `urlcode-dynamic-link` extension package that replaced it
154
- has been retired and unpublished.
155
-
156
- The health version combines route-definition and asset-representation digests;
157
- it does not identify the complete function/runtime release. Record runtime commit,
158
- application commit, dependency locks and image digest in your deployment system.
159
-
160
- ## Deployment and rollback procedure
161
-
162
- 1. Build a candidate from pinned runtime/application revisions and lockfiles.
163
- 2. Validate its config/bindings and run local HTTP tests without external redirects.
164
- 3. Start it on an alternate private port/container. Check readiness and representative
165
- redirect/function behavior through the intended proxy configuration:
166
- `urlcode verify-deployment --project . --target https://candidate.host` compares
167
- version, fixtures, policy headers and site files with the project
168
- ([deployment checks](DEPLOYMENT-CHECKS.md)).
169
- 4. Switch proxy traffic after checks pass. Drain the previous instance before stopping.
170
- 5. If checks or observed behavior fail, route traffic back to the retained previous
171
- instance/image and its compatible secret bindings.
172
-
173
- This is an operator procedure, not an implemented deployment control plane.
174
- Rollback cannot undo a function's external side effects or migrate an app's
175
- state automatically. Plan those independently. Keep Git definitions backed up;
176
- back up any app-owned persistent state separately. YAML routes require no database.
177
-
178
- ## Capacity and incident planning
179
-
180
- See [capacity and concurrency](CAPACITY.md) for hard limits, worker occupancy,
181
- no-queue rejection, memory/reload budgets and theoretical sizing. See
182
- [DDoS and recovery](RESILIENCE.md) for ingress responsibilities, incident response,
183
- rollback/restore procedures, recovery objectives and drills.
184
-
185
- See the [release-readiness register](RELEASE-READINESS.md) for evidence and open gates.
186
-
187
- ## Remaining production validation
188
-
189
- Before approving a production deployment: run sustained soak/load tests on its
190
- hardware, obtain independent security review, exercise failure/restart and
191
- upgrade/rollback, establish a clear support/reporting policy, and add the needed
192
- operational metrics. Provider adapters remain separate roadmap work. See
193
- [roadmap](../ROADMAP.md). No claim of high
194
- availability, zero downtime or provider portability beyond the Node process
195
- adapter is made by the current release.
196
-
197
- ## Security review
198
-
199
- The [2026-09-16 internal audit](SECURITY-AUDIT.md) records fixes, regression evidence
200
- and remaining security/operational gates. This is not an independent assessment.
201
-