@jimhoyd/urlcode 0.4.1 → 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 (212) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +60 -7
  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 +31 -28
  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 +29 -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 +138 -3
  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/policy.js +16 -0
  25. package/dist/project-tests.js +35 -11
  26. package/dist/readiness.js +188 -32
  27. package/dist/recipes.js +1 -1
  28. package/dist/router.js +17 -0
  29. package/dist/runtime.js +26 -3
  30. package/dist/scaffold.js +0 -0
  31. package/dist/server.js +26 -3
  32. package/dist/site.js +0 -0
  33. package/dist/tooling.js +3 -1
  34. package/dist/types/agent-context.d.ts +44 -0
  35. package/dist/types/authoring.d.ts +3 -1
  36. package/dist/types/body-schema.d.ts +58 -0
  37. package/dist/types/config.d.ts +10 -2
  38. package/dist/types/context.d.ts +1 -1
  39. package/dist/types/errors.d.ts +9 -1
  40. package/dist/types/examples.d.ts +1 -1
  41. package/dist/types/extensions.d.ts +105 -2
  42. package/dist/types/http-policy.d.ts +3 -0
  43. package/dist/types/init-with.d.ts +10 -1
  44. package/dist/types/pattern-guard.d.ts +10 -0
  45. package/dist/types/project-tests.d.ts +9 -0
  46. package/dist/types/readiness.d.ts +71 -0
  47. package/dist/types/recipes.d.ts +1 -1
  48. package/dist/types/runtime.d.ts +4 -0
  49. package/dist/types/server.d.ts +9 -1
  50. package/dist/types/tooling.d.ts +2 -0
  51. package/dist/types/types.d.ts +13 -0
  52. package/dist/types.js +10 -2
  53. package/dist/typescript-authoring.js +5 -3
  54. package/dist/verify-deployment.js +16 -10
  55. package/examples/body-validation/README.md +16 -0
  56. package/examples/body-validation/example.yaml +17 -0
  57. package/examples/body-validation/tests/requests.json +15 -0
  58. package/examples/body-validation/urlcode.yaml +34 -0
  59. package/examples/coverage-waiver/README.md +8 -0
  60. package/examples/coverage-waiver/example.yaml +16 -0
  61. package/examples/coverage-waiver/functions/notes.mjs +2 -0
  62. package/examples/coverage-waiver/tests/requests.json +3 -0
  63. package/examples/coverage-waiver/urlcode.yaml +10 -0
  64. package/examples/data-dir/README.md +39 -0
  65. package/examples/data-dir/data/welcome.txt +1 -0
  66. package/examples/data-dir/example.yaml +22 -0
  67. package/examples/data-dir/functions/note.mjs +18 -0
  68. package/examples/data-dir/tests/requests.json +6 -0
  69. package/examples/data-dir/urlcode.yaml +6 -0
  70. package/examples/lifecycle/README.md +15 -0
  71. package/examples/lifecycle/example.yaml +19 -0
  72. package/examples/lifecycle/functions/notes.mjs +29 -0
  73. package/examples/lifecycle/tests/requests.json +12 -0
  74. package/examples/lifecycle/urlcode.yaml +29 -0
  75. package/examples/not-found/README.md +10 -0
  76. package/examples/not-found/example.yaml +17 -0
  77. package/examples/not-found/public/404.html +3 -0
  78. package/examples/not-found/public/index.html +3 -0
  79. package/examples/not-found/tests/requests.json +7 -0
  80. package/examples/not-found/urlcode.yaml +7 -0
  81. package/examples/shared-blocks/README.md +11 -0
  82. package/examples/shared-blocks/example.yaml +17 -0
  83. package/examples/shared-blocks/tests/requests.json +7 -0
  84. package/examples/shared-blocks/urlcode.yaml +37 -0
  85. package/llms-full.txt +436 -149
  86. package/llms.txt +44 -6
  87. package/package.json +15 -22
  88. package/recipes/static-page/README.md +9 -0
  89. package/recipes/static-page/public/index.html +11 -0
  90. package/recipes/static-page/recipe.yaml +21 -0
  91. package/recipes/static-page/tests/requests.json +22 -0
  92. package/recipes/static-page/urlcode.yaml +7 -0
  93. package/recipes/static-plus-api/README.md +6 -0
  94. package/recipes/static-plus-api/urlcode.yaml +4 -0
  95. package/recipes/store-crud/README.md +53 -0
  96. package/recipes/store-crud/recipe.yaml +31 -0
  97. package/recipes/store-crud/tests/requests.json +18 -0
  98. package/recipes/store-crud/urlcode.yaml +18 -0
  99. package/schemas/urlcode.schema.json +125 -60
  100. package/skills/urlcode/SKILL.md +53 -26
  101. package/starters/default/AGENTS.md +43 -43
  102. package/starters/page/README.md +14 -0
  103. package/starters/page/public/index.html +12 -0
  104. package/starters/page/tests/requests.json +17 -0
  105. package/starters/page/urlcode.yaml +6 -0
  106. package/.claude-plugin/marketplace.json +0 -18
  107. package/CONTRIBUTING.md +0 -112
  108. package/ROADMAP.md +0 -66
  109. package/docs/AI-AUTHORING.md +0 -338
  110. package/docs/ASSETS.md +0 -107
  111. package/docs/AUTH-BACKUP.md +0 -32
  112. package/docs/AWS.md +0 -86
  113. package/docs/BEST-PRACTICES.md +0 -276
  114. package/docs/BULK.md +0 -79
  115. package/docs/CAPABILITIES.md +0 -192
  116. package/docs/CAPACITY.md +0 -305
  117. package/docs/CI-FOLLOWUP-2026-09-19.md +0 -97
  118. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +0 -322
  119. package/docs/CI.md +0 -147
  120. package/docs/CLOUDFLARE.md +0 -109
  121. package/docs/CODEBASE-AUDIT-2026-09-20.md +0 -284
  122. package/docs/COMPLIANCE.md +0 -239
  123. package/docs/COMPOSING-A-SITE.md +0 -278
  124. package/docs/CONDITIONS.md +0 -74
  125. package/docs/DEPLOYMENT-CHECKS.md +0 -108
  126. package/docs/DEVELOPMENT-PIPELINE.md +0 -270
  127. package/docs/EGRESS.md +0 -125
  128. package/docs/EXTENSIONS.md +0 -438
  129. package/docs/FRAMEWORK.md +0 -217
  130. package/docs/FUNCTION-SECURITY.md +0 -254
  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 -224
  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 -150
  151. package/docs/README.md +0 -87
  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-READINESS.md +0 -117
  156. package/docs/RELEASE-SECURITY.md +0 -96
  157. package/docs/RESILIENCE.md +0 -161
  158. package/docs/ROUTING.md +0 -92
  159. package/docs/SANDBOX-REVIEW.md +0 -72
  160. package/docs/SCAFFOLDING.md +0 -70
  161. package/docs/SECURITY-AUDIT.md +0 -164
  162. package/docs/SITE.md +0 -150
  163. package/docs/SPECIFICATION.md +0 -359
  164. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +0 -288
  165. package/docs/SPIKE-BUSINESS-SUITE.md +0 -1029
  166. package/docs/SPIKE-CORE-LAYERING.md +0 -368
  167. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +0 -211
  168. package/docs/STANDARDS.md +0 -311
  169. package/docs/STARTERS.md +0 -83
  170. package/docs/STATIC.md +0 -105
  171. package/docs/TOOLING.md +0 -298
  172. package/docs/TUNNELS.md +0 -72
  173. package/docs/TYPESCRIPT-AUTHORING.md +0 -87
  174. package/docs/TYPESCRIPT.md +0 -123
  175. package/docs/VERCEL.md +0 -114
  176. package/docs/VERSION-ALIGNMENT.md +0 -80
  177. package/docs/YAML-GUIDE.md +0 -57
  178. package/docs/YAML-REFERENCE.md +0 -449
  179. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +0 -68
  180. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +0 -102
  181. package/docs/archive/2026-09-19/NEXT-PHASE-PLAN.md +0 -108
  182. package/docs/archive/2026-09-19/NEXT-STEPS.md +0 -646
  183. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +0 -277
  184. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +0 -186
  185. package/docs/archive/2026-09-19/ROADMAP.md +0 -387
  186. package/docs/archive/2026-09-19/SPIKE-EXTENSION-MODEL.md +0 -430
  187. package/docs/archive/2026-09-19/SPIKE-EXTENSIONS.md +0 -492
  188. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +0 -365
  189. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +0 -778
  190. package/docs/archive/2026-09-19/USABILITY-REVIEW.md +0 -139
  191. package/docs/archive/README.md +0 -28
  192. package/docs/policies/agents.md +0 -182
  193. package/docs/policies/cache.md +0 -152
  194. package/docs/policies/compression.md +0 -169
  195. package/docs/policies/contract.md +0 -52
  196. package/docs/policies/hardened.md +0 -56
  197. package/docs/policies/interoperability.md +0 -169
  198. package/docs/policies/operations.md +0 -45
  199. package/docs/policies/security.md +0 -161
  200. package/docs/policies/throttle.md +0 -103
  201. package/docs/yaml/assets.md +0 -36
  202. package/docs/yaml/conditions.md +0 -20
  203. package/docs/yaml/functions.md +0 -168
  204. package/docs/yaml/middleware.md +0 -31
  205. package/docs/yaml/organization.md +0 -74
  206. package/docs/yaml/policies.md +0 -37
  207. package/docs/yaml/redirects.md +0 -64
  208. package/docs/yaml/responses.md +0 -57
  209. package/docs/yaml/site.md +0 -24
  210. package/packaging/claude-plugin/.claude-plugin/plugin.json +0 -19
  211. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +0 -120
  212. 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,224 +0,0 @@
1
- # Decisions to align
2
-
3
- Reviewed 2026-09-19 against core `db375bf` and the current public package sources.
4
- This is the maintainer's decision list, not a second implementation backlog.
5
- The [roadmap](../ROADMAP.md) gives sequence and the [archive](archive/README.md)
6
- keeps earlier discussions. Recommendations below are not accepted decisions.
7
-
8
- ## Principles already settled, in plain language
9
-
10
- - **Describe first, code only when needed.** Use a supported YAML feature or
11
- extension before writing plumbing. Custom application code is still welcome.
12
- - **Your application code runs like normal Node code.** Functions and middleware
13
- are trusted by default. `sandbox: true` deliberately restricts a route's whole
14
- function/middleware chain. Request data still needs validation in either mode.
15
- - **A grant controls what URLCode supplies, not what trusted code can access.**
16
- Trusted code can independently read the host environment, files and network.
17
- Opt-in sandboxing retains its existing isolation and revision-pinned grants.
18
- - **Portable does not mean every host supports every feature.** Keep infrastructure
19
- out of route YAML and reject unsupported targets before activation.
20
- - **Core works alone; optional packages add accounts, admin and presentation.**
21
- Core never imports those implementations. Shared UI belongs in `urlcode-ui`.
22
- - **Documentation stays beside the owning code.** Core guides live here;
23
- extension contracts and implementation status live in their repositories.
24
- `urlcode-docs` is deleted. `urlcode-short` and `urlcode-dynamic-link` are retired.
25
- - **Passing tests proves the tested behavior.** It does not prove deployment,
26
- accessibility, hostile tenant isolation or independent security assessment.
27
- - **Keep the free runtime useful.** Apache-2.0 remains unchanged; no mandatory
28
- hosted account, paid capability gate or provider lock-in belongs in core.
29
-
30
- ## Decisions still needed
31
-
32
- | Decision | What the code says today | Recommendation and consequence |
33
- |---|---|---|
34
- | 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. |
35
- | 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). |
36
- | Fold extension schemas into retrieved context? | `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. |
37
- | 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. |
38
-
39
- The broader [AI benchmark proposal](SPIKE-AI-FRAMEWORK-BENCHMARK.md) also needs a
40
- chosen application, model-run budget and execution authorization. The existing
41
- small-task harness can supply evidence without committing to that larger study.
42
-
43
- ## Closed questions removed from the active list
44
-
45
- - Publishing convention is recorded in [version alignment](VERSION-ALIGNMENT.md):
46
- publishable manifests on main, releases through reviewed tags/workflows.
47
- - Core `0.4.0-alpha.2` and current extension releases exist; publishing that
48
- already-shipped version is not a next step.
49
- - Auth/admin kit adoption and shared form helpers are implemented in their code.
50
- - The UI primitive fallback is retired, which settles the question this table
51
- carried. Auth and admin now render every screen through the kit and refuse
52
- activation without it (`packages/auth/src/auth.ts`: "there is no
53
- shared-primitive fallback"), so "keep both" no longer describes the code.
54
- - The template pins `0.4.0-alpha.2`. Its skill differences were read against that
55
- pin: omitted handlers and advice about the removed management API are stale,
56
- not intentional older-version behavior.
57
- - The guidance checks run through `npm run check` inside `verify`; a regex check
58
- is not a schema validator for every example. Extending its coverage is tracked
59
- separately, not a reason to weaken review or bypass required checks.
60
-
61
- ## Accepted: one Node deployment per project
62
-
63
- **Decided 2026-09-19.** Projects that use `function` or `middleware` deploy as
64
- **one trusted Node process** — a container or a VM running the project as it
65
- runs locally. That is the supported execution model, and it needs no new work:
66
- it is what the runtime already does.
67
-
68
- **Per-route Lambda compilation is not pursued.** The alternative on the table
69
- was a build step emitting one Lambda per `function` route
70
- ([the proposal](archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md)). It is declined for now, on three
71
- grounds the proposal itself states:
72
-
73
- 1. It would replace the sandbox guarantee rather than preserve it, and lose the
74
- fresh-per-invocation state that `sandbox: true` currently guarantees.
75
- 2. It would make this project the author of generated IAM roles — a
76
- security-critical output it has never owned.
77
- 3. It would trade an honest refusal for a larger claim nobody has deployed.
78
-
79
- Against that, a single Node deployment supports every route type today with no
80
- compiler, no generated infrastructure and no second isolation story to document.
81
-
82
- **What follows from this decision:**
83
-
84
- - AWS and Vercel continue to refuse `function` and `middleware` at activation,
85
- naming the route (`src/capabilities.ts`, `activateNativeOnly` in
86
- `src/adapters.ts`). That refusal is now a **deliberate position**, not a gap
87
- awaiting an adapter. Documentation should say so rather than implying the
88
- support is coming.
89
- - Serverless targets remain first-class for the declarative route types they can
90
- actually serve; nothing about static or native-only deployment changes.
91
- - [SPIKE-LAMBDA-COMPILE.md](archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md) is kept as the analysis
92
- behind this decision, not as a plan. Reopen it only on evidence of real demand
93
- for URLCode `function` routes specifically on AWS serverless — the proposal's
94
- own §6 already scopes what a first attempt would be.
95
-
96
- This decision is about the *execution model*, not about AWS. Deploying the Node
97
- process to AWS (ECS, EC2, App Runner) is an operator choice this fully supports.
98
-
99
- ## Accepted: per-package release tags
100
-
101
- **Decided 2026-09-19.** Workspace packages under `packages/` release on
102
- Changesets' own `<package name>@<version>` form — for example
103
- `@jimhoyd/urlcode-ui@0.1.0-alpha.6`. Core keeps bare `v*`.
104
-
105
- **The problem.** Core and all three extensions arrived here triggering on
106
- `tags: ['v*']`, and their alpha tags overlap outright: ui shipped
107
- `v0.1.0-alpha.2` through `-alpha.5`, admin `v0.1.0-alpha.1` and `-alpha.3`,
108
- auth `v0.1.0-alpha.1` through `-alpha.3`. Across four repositories that was
109
- fine. In one repository a single bare tag push starts more than one release
110
- workflow. Each one fails closed on its own tag-matches-manifest check, so
111
- nothing can mis-publish — but "two workflows race and one errors on every
112
- release" is not a release process, and the failure is confusing rather than
113
- informative.
114
-
115
- **Why Changesets' form rather than a prefix like `ui-v0.1.0-alpha.6`.** Both
116
- work and both are valid ref names. The deciding factor is that Changesets is
117
- already the chosen release flow, and `changeset tag` emits the
118
- `<name>@<version>` form natively. Picking anything else means writing and
119
- maintaining a translation layer between the tool that computes the version and
120
- the tag that triggers the publish — new code whose only job is to disagree
121
- with a default. The spike chose Changesets partly because it is "cheap and
122
- low-risk for an agent or a human to generate correctly"; hand-rolling the tag
123
- shape undercuts exactly that.
124
-
125
- **Why the two schemes cannot collide.** A scoped package name begins with `@`,
126
- and GitHub's `v*` filter requires a leading `v`, so no tag can match both.
127
- Verified rather than assumed, including that `*` does not match `/` in a filter
128
- pattern, so `@jimhoyd/urlcode-ui@*` matches the version segment only.
129
-
130
- **Core's asymmetry is forced, not preferred.** Under layout A core is the
131
- repository root rather than a workspace member, so Changesets does not manage
132
- it and `changeset version` will not bump it. Core therefore keeps the tag
133
- scheme and release workflow it already had.
134
-
135
- [`scripts/check-release-tags.ts`](../scripts/check-release-tags.ts) enforces
136
- this in `npm run check`: it rejects a workspace package workflow that does not
137
- trigger on its own `<name>@*`, rejects any workflow other than core's claiming
138
- `v*`, and independently asserts that no two filters can match the same tag. The
139
- reasoning above is the kind of prose that rots as soon as `auth` and `admin`
140
- arrive, which is the whole argument this repository makes for enforcing checks
141
- over documented intent.
142
-
143
- ## Done: the monorepo migration is complete
144
-
145
- **Closed 2026-09-19.** `urlcode-ui`, `urlcode-auth` and `urlcode-admin` are
146
- workspace packages under `packages/`, and all three have been released from
147
- this repository — `@jimhoyd/urlcode-ui@0.1.0-alpha.6`,
148
- `@jimhoyd/urlcode-auth@0.1.0-alpha.6`, `@jimhoyd/urlcode-admin@0.1.0-alpha.4`,
149
- each on `alpha` with `latest` deliberately held behind. Core's dist-tags are
150
- unchanged. The three source repositories are gone; their history survives only
151
- as verified `git bundle`s, because the repository allows squash merges only and
152
- the imported commits did not survive onto `main`.
153
-
154
- The operational runbook is [DEVELOPMENT-PIPELINE.md](DEVELOPMENT-PIPELINE.md)
155
- and [RELEASE-SECURITY.md](RELEASE-SECURITY.md). The plan itself is archived at
156
- [archive/2026-09-19/SPIKE-MONOREPO.md](archive/2026-09-19/SPIKE-MONOREPO.md),
157
- whose closing note records what the plan got wrong — chiefly that its
158
- strongest argument, the reach of the enforcing checks, only became true after
159
- both checkers were changed to discover workspace packages.
160
-
161
- The section below is kept for the middleware decision it records, which is
162
- still the reason there is no `packages/middleware`.
163
-
164
- ## Accepted: monorepo first — middleware withdrawn rather than consolidated
165
-
166
- The maintainer confirmed that monorepo work is starting now. The earlier
167
- recommendation to keep repositories separate for now is superseded.
168
-
169
- **Reversed 2026-09-19: the middleware half of this decision no longer applies.**
170
- This section used to say "move middleware into the monorepo as its own package
171
- first, preserving its existing API and behavior," and explicitly: "do not
172
- unpublish or retire the middleware package as part of the initial move."
173
- That instruction was overtaken. `@jimhoyd/urlcode-middleware` has been
174
- **unpublished** from npm at `0.1.0-alpha.2` and `jimhoyd-com/urlcode-middleware`
175
- **deleted** — the package was withdrawn outright instead of migrated, so there
176
- is no `packages/middleware` to create and no subsequent fold-into-core step.
177
- The reversal is recorded here rather than deleted because the instruction it
178
- replaces was explicit, and a reader who remembers it should be able to see that
179
- it was changed deliberately and not simply forgotten.
180
-
181
- Nothing was lost in capability terms: per-route middleware is **native to
182
- core** via the `middleware:` array ([MIDDLEWARE.md](MIDDLEWARE.md)), and the
183
- deleted package only ever offered the same behavior through the extension
184
- seam. Trust remains the default and explicit sandbox choices keep their
185
- meaning. The generic extension wrapping hook (`ExtensionInstance.middleware`,
186
- `RuntimeExtension.cacheSensitive`) stays in core's contract for other
187
- extensions — it is no longer exercised by any shipped package, which is worth
188
- knowing before it is assumed to be covered. Static targets continue rejecting
189
- request-time middleware because there is no server to run it.
190
-
191
- Migration scope is therefore **core, auth, admin and UI**. Template and the
192
- distribution tap stay outside that package move. The observed stale peer pins
193
- and checkout-limited guidance checks strengthen the case for shared
194
- verification: carry those checks across the new package paths, since merely
195
- moving files does not prove every generated skill is covered. The cleanup PRs
196
- that superseded the earlier zero-open-PR survey have since merged, and a fresh
197
- survey again reports zero open pull requests across all four in-scope
198
- repositories — re-run it per repository immediately before that repository
199
- moves rather than trusting this line.
200
-
201
- The [archived monorepo plan](archive/2026-09-19/SPIKE-MONOREPO.md) records
202
- migration context.
203
- [Issue 172](https://github.com/jimhoyd-com/urlcode/issues/172), which tracked
204
- "consolidate middleware into core after moving it into the monorepo," was
205
- **closed on 2026-09-19** as moot — there was nothing left to consolidate.
206
- Migration starting is not a claim that it has landed.
207
-
208
- ## Source review baseline
209
-
210
- | Repository | Reviewed commit | Code checked |
211
- |---|---|---|
212
- | core | `db375bf` | Runtime dispatch, schema normalization, capabilities, static compiler, prerender, MCP and resource generators |
213
- | auth | `71957dd` | Lifecycle hooks, UI rendering and shared helper imports |
214
- | admin | `f3b4882` | UI rendering, auth-service integration and shared helper imports |
215
- | UI | `0e96f7f` | Shared forms, kit/host exports and copied core contract |
216
- | ~~middleware~~ | `f201f4b` | Extension wrapping, per-entry sandbox dispatch and scaffolding — **repository deleted 2026-09-19; this baseline is unreachable except through the local `urlcode-middleware.bundle`** |
217
- | template | `4e09e50` | Exact core pin, generated guide and both vendored skills |
218
-
219
- The Homebrew tap (`73eaaef`) still selects stable core `0.3.0`; its old trust
220
- behavior belongs to that pin and must not be rewritten as alpha.2 behavior.
221
- The other organization tap and Scoop bucket contain Gitroll, not URLCode.
222
- The deleted documentation repository is historical context, not a second source
223
- of current contracts; its former GitHub links no longer resolve. This review is targeted source inspection, not an audit of
224
- every execution path or an independent security assessment.
@@ -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.