@jimhoyd/urlcode 0.4.0-alpha.1 → 0.4.0-alpha.3

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 (232) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +36 -22
  2. package/.claude/skills/urlcode-operations/SKILL.md +16 -22
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CONTRIBUTING.md +20 -16
  5. package/README.md +83 -80
  6. package/ROADMAP.md +61 -331
  7. package/SECURITY.md +23 -8
  8. package/dist/BUILD-MANIFEST.json +42 -47
  9. package/dist/agents-guide.js +18 -18
  10. package/dist/build-static.js +136 -0
  11. package/dist/capabilities.js +85 -31
  12. package/dist/capability-query.js +0 -1
  13. package/dist/cli.js +24 -32
  14. package/dist/compliance-rules/baseline.js +2 -10
  15. package/dist/compliance-rules/privacy.js +5 -16
  16. package/dist/compliance-rules/shared.js +0 -2
  17. package/dist/compliance.js +6 -8
  18. package/dist/config.js +15 -8
  19. package/dist/context.js +9 -10
  20. package/dist/examples.js +2 -2
  21. package/dist/explain-cli.js +7 -5
  22. package/dist/explain.js +10 -5
  23. package/dist/extensions.js +61 -2
  24. package/dist/function-sources.js +34 -2
  25. package/dist/function-worker.js +3 -1
  26. package/dist/functions.js +84 -13
  27. package/dist/guest-api.js +29 -3
  28. package/dist/index.js +3 -6
  29. package/dist/manifest.js +11 -7
  30. package/dist/mcp-authoring.js +2 -2
  31. package/dist/mcp.js +1 -1
  32. package/dist/observability.js +1 -21
  33. package/dist/policies/cache.js +8 -3
  34. package/dist/policies.js +3 -1
  35. package/dist/policy.js +28 -9
  36. package/dist/prerender.js +4 -0
  37. package/dist/project-tests.js +3 -3
  38. package/dist/readiness.js +35 -9
  39. package/dist/route-diff.js +12 -5
  40. package/dist/router.js +5 -7
  41. package/dist/runtime.js +77 -58
  42. package/dist/sandbox.js +48 -0
  43. package/dist/scaffold.js +0 -0
  44. package/dist/scripts/operational-drills.js +12 -54
  45. package/dist/server.js +3 -29
  46. package/dist/tooling.js +1 -1
  47. package/dist/trusted-functions.js +210 -0
  48. package/dist/types/build-static.d.ts +43 -0
  49. package/dist/types/capabilities.d.ts +14 -5
  50. package/dist/types/compliance-rules/shared.d.ts +0 -2
  51. package/dist/types/compliance.d.ts +0 -3
  52. package/dist/types/config.d.ts +2 -1
  53. package/dist/types/context.d.ts +2 -1
  54. package/dist/types/examples.d.ts +1 -1
  55. package/dist/types/explain.d.ts +6 -0
  56. package/dist/types/extensions.d.ts +55 -0
  57. package/dist/types/function-sources.d.ts +4 -0
  58. package/dist/types/functions.d.ts +48 -5
  59. package/dist/types/guest-api.d.ts +1 -0
  60. package/dist/types/index.d.ts +3 -6
  61. package/dist/types/manifest.d.ts +5 -3
  62. package/dist/types/observability.d.ts +1 -14
  63. package/dist/types/project-tests.d.ts +1 -2
  64. package/dist/types/readiness.d.ts +12 -3
  65. package/dist/types/router.d.ts +2 -1
  66. package/dist/types/runtime.d.ts +0 -27
  67. package/dist/types/sandbox.d.ts +12 -0
  68. package/dist/types/scaffold.d.ts +0 -2
  69. package/dist/types/server.d.ts +1 -4
  70. package/dist/types/tooling.d.ts +3 -3
  71. package/dist/types/trusted-functions.d.ts +29 -0
  72. package/dist/types/types.d.ts +22 -8
  73. package/dist/types/verify-deployment.d.ts +2 -2
  74. package/dist/types.js +25 -5
  75. package/dist/typescript-authoring.js +67 -18
  76. package/dist/verify-deployment.js +3 -3
  77. package/docs/AI-AUTHORING.md +109 -10
  78. package/docs/ASSETS.md +2 -1
  79. package/docs/AUTH-BACKUP.md +32 -0
  80. package/docs/AWS.md +4 -4
  81. package/docs/BEST-PRACTICES.md +17 -8
  82. package/docs/CAPABILITIES.md +30 -17
  83. package/docs/CAPACITY.md +128 -31
  84. package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
  85. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
  86. package/docs/CI.md +8 -3
  87. package/docs/CLOUDFLARE.md +1 -2
  88. package/docs/CODEBASE-AUDIT-2026-09-20.md +278 -0
  89. package/docs/COMPLIANCE.md +6 -9
  90. package/docs/DEPLOYMENT-CHECKS.md +1 -1
  91. package/docs/DEVELOPMENT-PIPELINE.md +181 -0
  92. package/docs/EXTENSIONS.md +193 -11
  93. package/docs/FRAMEWORK.md +53 -33
  94. package/docs/FUNCTION-SECURITY.md +173 -32
  95. package/docs/INSTALL.md +0 -5
  96. package/docs/LOAD-TESTING.md +4 -4
  97. package/docs/LOCAL-DEVELOPMENT.md +3 -0
  98. package/docs/MIDDLEWARE.md +27 -16
  99. package/docs/MONITORING.md +2 -19
  100. package/docs/OBSERVABILITY.md +7 -16
  101. package/docs/OPEN-DECISIONS.md +184 -0
  102. package/docs/OPERATIONAL-PROOF.md +26 -30
  103. package/docs/OPERATIONS.md +23 -32
  104. package/docs/POLICIES.md +27 -8
  105. package/docs/PRERENDER.md +25 -13
  106. package/docs/PROJECT-DIRECTION.md +35 -10
  107. package/docs/READINESS.md +6 -2
  108. package/docs/README.md +20 -41
  109. package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
  110. package/docs/RELEASE-READINESS.md +50 -30
  111. package/docs/RELEASE-SECURITY.md +72 -86
  112. package/docs/RESILIENCE.md +16 -15
  113. package/docs/ROUTING.md +7 -10
  114. package/docs/SANDBOX-REVIEW.md +19 -6
  115. package/docs/SCAFFOLDING.md +0 -2
  116. package/docs/SECURITY-AUDIT.md +42 -2
  117. package/docs/SPECIFICATION.md +63 -27
  118. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +288 -0
  119. package/docs/SPIKE-BUSINESS-SUITE.md +16 -0
  120. package/docs/SPIKE-CORE-LAYERING.md +368 -0
  121. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +211 -0
  122. package/docs/SPIKE-LAMBDA-COMPILE.md +12 -2
  123. package/docs/STARTERS.md +1 -1
  124. package/docs/STATIC.md +105 -0
  125. package/docs/TOOLING.md +17 -12
  126. package/docs/TUNNELS.md +0 -3
  127. package/docs/TYPESCRIPT-AUTHORING.md +32 -12
  128. package/docs/TYPESCRIPT.md +25 -4
  129. package/docs/VERCEL.md +4 -5
  130. package/docs/VERSION-ALIGNMENT.md +46 -0
  131. package/docs/YAML-GUIDE.md +0 -3
  132. package/docs/YAML-REFERENCE.md +16 -6
  133. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
  134. package/docs/{MANAGEMENT-SECURITY.md → archive/2026-09-19/MANAGEMENT-SECURITY.md} +20 -0
  135. package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +26 -8
  136. package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +274 -43
  137. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
  138. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
  139. package/docs/archive/2026-09-19/ROADMAP.md +386 -0
  140. package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +12 -2
  141. package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +28 -12
  142. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +776 -0
  143. package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +18 -2
  144. package/docs/archive/README.md +27 -0
  145. package/docs/policies/agents.md +1 -1
  146. package/docs/policies/cache.md +13 -0
  147. package/docs/policies/compression.md +3 -2
  148. package/docs/policies/security.md +3 -2
  149. package/examples/assets/example.yaml +2 -2
  150. package/examples/aws/example.yaml +1 -1
  151. package/examples/cloudflare/example.yaml +1 -1
  152. package/examples/conditions/example.yaml +1 -1
  153. package/examples/cookbook/example.yaml +1 -1
  154. package/examples/cookbook/middleware/bucket.mjs +12 -2
  155. package/examples/cookbook/middleware/locale.mjs +7 -3
  156. package/examples/cookbook/route-index.json +1 -1
  157. package/examples/cookbook/routes/middleware.yaml +1 -1
  158. package/examples/egress/example.yaml +1 -1
  159. package/examples/extensions/example.yaml +1 -1
  160. package/examples/prerender/README.md +14 -6
  161. package/examples/prerender/example.yaml +1 -1
  162. package/examples/prerender/functions/page.mjs +4 -2
  163. package/examples/prerender/middleware/template.mjs +1 -1
  164. package/examples/prerender/prerender.mjs +1 -1
  165. package/examples/prerender/urlcode.yaml +8 -4
  166. package/examples/provider-conformance/example.yaml +1 -1
  167. package/examples/vercel/example.yaml +1 -1
  168. package/llms-full.txt +686 -239
  169. package/llms.txt +27 -15
  170. package/package.json +32 -5
  171. package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
  172. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +36 -22
  173. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +16 -22
  174. package/recipes/authenticated-json-api/README.md +4 -3
  175. package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
  176. package/recipes/authenticated-json-api/recipe.yaml +2 -2
  177. package/recipes/contact-form/functions/contact.mjs +2 -1
  178. package/recipes/contact-form/recipe.yaml +3 -3
  179. package/recipes/cors-api/README.md +2 -2
  180. package/recipes/cors-api/recipe.yaml +2 -2
  181. package/recipes/health-page/README.md +1 -1
  182. package/recipes/health-page/recipe.yaml +1 -1
  183. package/recipes/json-api/README.md +1 -1
  184. package/recipes/json-api/recipe.yaml +4 -4
  185. package/recipes/middleware/README.md +8 -4
  186. package/recipes/middleware/middleware/bucket.mjs +12 -2
  187. package/recipes/middleware/middleware/locale.mjs +7 -3
  188. package/recipes/middleware/recipe.yaml +1 -1
  189. package/recipes/protected-download/README.md +1 -1
  190. package/recipes/protected-download/recipe.yaml +2 -2
  191. package/recipes/redirect/recipe.yaml +1 -1
  192. package/recipes/static-plus-api/README.md +2 -2
  193. package/recipes/static-plus-api/public/index.html +1 -1
  194. package/recipes/static-plus-api/recipe.yaml +2 -2
  195. package/recipes/static-plus-api/urlcode.yaml +1 -1
  196. package/recipes/typescript/README.md +2 -1
  197. package/recipes/typescript/recipe.yaml +5 -5
  198. package/recipes/webhook-receiver/README.md +5 -1
  199. package/recipes/webhook-receiver/recipe.yaml +2 -1
  200. package/recipes/webhook-receiver/urlcode.yaml +7 -0
  201. package/schemas/recipe.schema.json +4 -3
  202. package/schemas/urlcode.schema.json +14 -41
  203. package/skills/urlcode/SKILL.md +32 -11
  204. package/starters/default/AGENTS.md +18 -18
  205. package/starters/default/urlcode.yaml +0 -1
  206. package/dist/link-api.js +0 -136
  207. package/dist/link-cli.js +0 -141
  208. package/dist/link-events.js +0 -76
  209. package/dist/link-records.js +0 -31
  210. package/dist/link-store-worker.js +0 -150
  211. package/dist/link-store.js +0 -250
  212. package/dist/management-policy.js +0 -40
  213. package/dist/sqlite-version.js +0 -6
  214. package/dist/types/link-api.d.ts +0 -30
  215. package/dist/types/link-cli.d.ts +0 -37
  216. package/dist/types/link-events.d.ts +0 -27
  217. package/dist/types/link-records.d.ts +0 -11
  218. package/dist/types/link-store-worker.d.ts +0 -1
  219. package/dist/types/link-store.d.ts +0 -130
  220. package/dist/types/management-policy.d.ts +0 -8
  221. package/dist/types/sqlite-version.d.ts +0 -1
  222. package/docs/DYNAMIC-LINKS.md +0 -61
  223. package/docs/links/cli.md +0 -110
  224. package/docs/links/limits.md +0 -175
  225. package/docs/links/management-api.md +0 -80
  226. package/docs/links/pools.md +0 -75
  227. package/docs/links/setup.md +0 -135
  228. package/docs/yaml/links.md +0 -30
  229. package/examples/live-links/README.md +0 -11
  230. package/examples/live-links/example.yaml +0 -21
  231. package/examples/live-links/tests/requests.json +0 -6
  232. package/examples/live-links/urlcode.yaml +0 -16
package/docs/CAPACITY.md CHANGED
@@ -13,16 +13,56 @@ lookup after path parsing). Parameter candidates are grouped by segment count
13
13
  and scanned in specificity order; matching is O(P × L) in the worst case for P
14
14
  candidates and L segments. Static mount prefixes are scanned longest first.
15
15
 
16
- Plain redirects, declared responses, stored-link lookups and assets do not enter
17
- the sandbox. Stored links use a separate bounded database pools. A
18
- function or any attached middleware occupies one shared worker slot for its
19
- whole chain. Workers are shared by all programmable routes in that snapshot;
20
- there is no per-route fairness or reserved capacity. Awaiting guest timers still
21
- occupies the slot. A fresh guest and module initialization are part of each call.
16
+ Plain redirects, declared responses and assets do not enter
17
+ the sandbox or the trusted executor.
18
+
19
+ `function`/`middleware` routes have **two distinct capacity models**, chosen
20
+ per route by `sandbox` (docs/SPIKE-DEFAULT-TRUST-MODEL.md):
21
+
22
+ - **`sandbox: true` (the isolated worker pool, unchanged from every earlier
23
+ release):** a function or any attached middleware occupies one shared
24
+ worker slot for its whole chain. Workers are shared by all `sandbox: true`
25
+ routes in that snapshot; there is no per-route fairness or reserved
26
+ capacity. Awaiting guest timers still occupies the slot. A fresh guest and
27
+ module initialization are part of each call. See "Enforced limits and
28
+ defaults" below for the numbers (2 workers, 5 s deadline, 32 MiB heap).
29
+ - **`sandbox` false/absent (the trusted default):** the call runs in-process,
30
+ on the same event loop as everything else the server does — ordinary Node
31
+ concurrency, not a fixed worker-slot ceiling. There is no separate pool to
32
+ exhaust and no per-invocation heap/module reset: it is bounded by the same
33
+ `--max-in-flight` HTTP admission cap (default 64) that bounds every other
34
+ request, not by a `workers` count. A trusted call's declared `timeoutMs`
35
+ races the call's own promise rather than forcibly terminating a worker
36
+ thread — see "Trusted-path deadlines" below for what that does and does not
37
+ protect against.
38
+
39
+ This was architectural reasoning, not a measurement, until the run below:
40
+ see "Measured: sandboxed vs trusted dispatch" for the actual comparison.
22
41
 
23
42
  Node's main event loop remains a shared bottleneck for HTTP parsing, logging and
24
- native responses. Sandboxing contains application code authority and bounds
25
- individual execution; it does not make all host resources immune to exhaustion.
43
+ native responses. The sandbox contains a `sandbox: true` route's application
44
+ code authority and bounds its individual execution; the trusted default does
45
+ not attempt to, by design. Neither mode makes all host resources immune to
46
+ exhaustion.
47
+
48
+ ### Trusted-path deadlines
49
+
50
+ A sandboxed worker's deadline is enforced by an interrupt handler the WASM
51
+ engine checks between guest operations, backed by an independent outer
52
+ termination that kills the worker thread if the guest never yields — the
53
+ worker (and its slot) can be forcibly reclaimed even from a stuck call. A
54
+ trusted, in-process call has no such mechanism available: `timeoutMs` starts
55
+ a race between the call's promise and a timer, so a call that never resolves
56
+ (an unresolved promise, an awaited operation that never completes) is
57
+ answered with a 504 on schedule, but a call that blocks the event loop
58
+ *synchronously* (an infinite `while` loop, a huge synchronous computation)
59
+ is not preempted — it keeps running, delays that timer's own firing, and
60
+ holds up every other request on the same process until it returns control to
61
+ the event loop or the process is restarted. This is a real, documented
62
+ difference from the sandboxed path's guarantee, not an oversight: Node has no
63
+ supported way to interrupt another turn of the same thread's event loop from
64
+ inside it. A route whose trusted code cannot be trusted to yield promptly is
65
+ exactly the kind of route `sandbox: true` exists for.
26
66
 
27
67
  ## Enforced limits and defaults
28
68
 
@@ -39,15 +79,17 @@ individual execution; it does not make all host resources immune to exhaustion.
39
79
  | Requests per socket | 1,000 | Connection recycling; not a requests-per-second limit |
40
80
  | Header / request receipt / keep-alive timeouts | 10 s / 15 s / 5 s | These are not an overall end-to-end response deadline |
41
81
  | Request body | 1 MiB default | Buffered; route maxBytes can tighten to 0–1 MiB |
42
- | Sandbox concurrency | 2 workers, no queue | Shared per snapshot; full pool returns 503 |
43
- | Execution deadline | 5 s default | Entire middleware + handler invocation; timeout returns 504 |
44
- | Guest heap / stack | 32 MiB / 512 KiB | Fresh per invocation; not a bound on total process RSS |
45
- | Outer worker old-generation V8 budget | 128 MiB | Separate from WASM/host/native allocations |
46
- | Function response | 1 MiB default, 16 KiB / 256 header pairs | Buffered text/JSON; YAML headers also bounded |
47
- | Middleware | 16 entries per route | One shared slot/deadline, not 16 independent workers |
48
- | Function sources | 128 modules, 1 MiB/module, 4 MiB total | Project snapshot, including middleware dependencies |
49
- | Worker startup | 5 s deadline | Failure rejects activation; no untrusted host fallback |
50
- | Worker replacement | Up to 3 exits/minute per slot trigger replacement | Further churn leaves the slot unavailable until reload/restart |
82
+ | Sandbox concurrency (`sandbox: true` only) | 2 workers, no queue | Shared per `sandbox: true` snapshot; full pool returns 503 |
83
+ | Sandbox execution deadline (`sandbox: true` only) | 5 s default | Entire middleware + handler invocation; forcibly terminates the worker; timeout returns 504 |
84
+ | Trusted concurrency (`sandbox` false/absent, the default) | Ordinary Node concurrency | Bounded by `--max-in-flight` (default 64), not a worker count; no separate pool to exhaust |
85
+ | Trusted execution deadline (`sandbox` false/absent) | 5 s default (same `timeoutMs` knob) | Races the call's promise; cannot preempt synchronous event-loop-blocking code (see "Trusted-path deadlines" above); timeout returns 504 |
86
+ | Guest heap / stack (`sandbox: true` only) | 32 MiB / 512 KiB | Fresh per invocation; not a bound on total process RSS |
87
+ | Outer worker old-generation V8 budget (`sandbox: true` only) | 128 MiB | Separate from WASM/host/native allocations |
88
+ | Function response | 1 MiB default, 16 KiB / 256 header pairs | Buffered text/JSON; YAML headers also bounded; applies to both execution modes |
89
+ | Middleware | 16 entries per route | One shared slot/deadline (`sandbox: true`) or one in-process call (trusted), not 16 independent workers either way |
90
+ | Function sources (`sandbox: true` only) | 128 modules, 1 MiB/module, 4 MiB total | Sandboxed snapshot, including middleware dependencies; a trusted route's own source is hashed for grant pinning but not bundled or budget-limited this way (see docs/FUNCTION-SECURITY.md) |
91
+ | Worker startup (`sandbox: true` only) | 5 s deadline | Failure rejects activation; no untrusted host fallback |
92
+ | Worker replacement (`sandbox: true` only) | Up to 3 exits/minute per slot trigger replacement | Further churn leaves the slot unavailable until reload/restart |
51
93
  | Assets | 16 MiB/file, 64 MiB unique contents | Buffered immutable snapshots; 10,000 static entries, depth 20 |
52
94
  | Logger buffering | Drop at 1 MiB stdout buffering | Reports logs_dropped when output recovers |
53
95
 
@@ -71,8 +113,70 @@ increasing a timeout also increases how long an attacker can occupy capacity.
71
113
  The CLI uses defaults. Keep settings identical across replicas unless testing a
72
114
  controlled rollout. See [operations](OPERATIONS.md).
73
115
 
116
+ ## Measured: sandboxed vs trusted dispatch
117
+
118
+ The claim above — that the trusted path has "no fixed worker-slot ceiling"
119
+ and scales as ordinary Node concurrency instead — was written as
120
+ architectural reasoning when the trusted-by-default execution model shipped
121
+ (docs/SPIKE-DEFAULT-TRUST-MODEL.md), not backed by a benchmark run. This is
122
+ that run: `benchmarks/sandbox-vs-trusted.ts`
123
+ (`npm run benchmark:sandbox-vs-trusted`), same 20 ms handler source on both a
124
+ `sandbox: true` route and a trusted route in the same server, default
125
+ settings (`workers: 2`, `maxInFlightRequests: 64`, `timeoutMs: 5000`), 2,000
126
+ requests per concurrency level after a 20-request warmup, keep-alive
127
+ connections. Raw output:
128
+ [benchmarks/results/2026-09-19-sandbox-vs-trusted.json](../benchmarks/results/2026-09-19-sandbox-vs-trusted.json).
129
+
130
+ One development machine: Intel Xeon @ 2.10 GHz, 4 vCPUs, 16 GiB RAM, Linux
131
+ x64, Node v22.22.2, 2026-09-19. This is one machine's numbers, not a
132
+ universal claim — re-run on deployment hardware before sizing anything.
133
+
134
+ | Concurrency | Sandboxed req/s (successful) | Sandboxed shed | Sandboxed p95 | Trusted req/s (successful) | Trusted shed | Trusted p95 |
135
+ |---:|---:|---:|---:|---:|---:|---:|
136
+ | 1 | 39 | 0 / 2000 | 26.9 ms | 47 | 0 / 2000 | 21.5 ms |
137
+ | 2 | 80 | 0 / 2000 | 26.5 ms | 95 | 0 / 2000 | 21.7 ms |
138
+ | 8 | 80 | 1,912 / 2000 | 30.2 ms | 379 | 0 / 2000 | 22.1 ms |
139
+ | 32 | 80 | 1,936 / 2000 | 43.5 ms | 1,465 | 0 / 2000 | 23.5 ms |
140
+ | 128 | 80 | 1,950 / 2000 | 70.0 ms | 2,615 | 521 / 2000 | 80.6 ms |
141
+
142
+ "Sandboxed req/s" and "shed" are separate columns for a reason: throughput
143
+ computed over successful responses only is flat at ~80 req/s from
144
+ concurrency 2 upward because the 2-worker ceiling admits at most two
145
+ in-flight calls at a time and returns 503 immediately for the rest, exactly
146
+ as "Enforced limits and defaults" describes (no queue). The ceiling is
147
+ visible starting at concurrency 8 in this run (already >95% shed) and only
148
+ gets worse as concurrency rises — 2 concurrent in-flight sandboxed calls is
149
+ the practical limit regardless of how much load is offered.
150
+
151
+ The trusted path has no such wall: it scales with offered concurrency —
152
+ 379 req/s at 8, 1,465 req/s at 32, both with zero shedding — until offered
153
+ concurrency (128) exceeds the `--max-in-flight` admission cap (64), at which
154
+ point it starts shedding too (521/2000, p95 rises to 80.6 ms) but keeps
155
+ substantially higher successful throughput (2,615 req/s) than the sandboxed
156
+ path ever reaches at any concurrency tested. This confirms the architectural
157
+ prediction rather than contradicting it: the trusted path's ceiling is the
158
+ admission cap, not a fixed worker count, and it sits far above the sandboxed
159
+ path's 2-worker ceiling in this run. It is still a real ceiling — raising
160
+ `--max-in-flight` moves it, it does not remove it — and it is shared with
161
+ every other route on the process, per "Native routes and mixed traffic"
162
+ below.
163
+
164
+ Caveats specific to this run: one handler shape (a 20 ms guest/host timer,
165
+ chosen to make both ceilings visible rather than to represent any particular
166
+ application), one machine, one process with both route types present
167
+ (so CPU contention between them is part of the trusted numbers, not
168
+ excluded), and no TLS, proxy or production logging in the path. Re-run with
169
+ your own handler's actual latency profile before sizing a deployment; see
170
+ "Establish a deployment budget" below.
171
+
74
172
  ## A useful theoretical model
75
173
 
174
+ This worker-slot model describes the `sandbox: true` path only. A trusted
175
+ route has no fixed worker count to plug in as W; its ceiling is ordinary Node
176
+ request concurrency bounded by `--max-in-flight`, not this model — see
177
+ "Measured: sandboxed vs trusted dispatch" above for what that ceiling looks
178
+ like in practice.
179
+
76
180
  Let W be worker slots, S the measured mean slot occupancy in seconds (including
77
181
  sandbox startup and cleanup effects), and lambda the offered programmable
78
182
  requests per second. An idealized worker ceiling is:
@@ -159,6 +263,13 @@ loop even though the HTTP listener is not restarted. Do not equate atomic swap
159
263
  with zero latency impact or incremental route updates. Prefer candidate replicas
160
264
  and traffic switching for production. `serve` does not watch configuration.
161
265
 
266
+ A trusted route's own entry file is re-imported fresh on every reload (see
267
+ [docs/FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)), matching the sandboxed
268
+ pool rebuilding its whole snapshot; a file that entry only imports is not,
269
+ since ordinary Node module resolution — not a per-reload snapshot — governs
270
+ it. Restart the process rather than reload after editing a trusted route's
271
+ dependency, not just its declared `source`.
272
+
162
273
  Before parser-worker limits were introduced, recorded 100k-route startup RSS was about 621 MiB on one development machine,
163
274
  above the illustrative 512 MiB container example. Route limits are acceptance
164
275
  caps, not a promise that the maximum fits your deployment. See [measurements](PERFORMANCE.md).
@@ -186,20 +297,6 @@ production exercise. The readiness endpoint can stay 200 while all worker slots
186
297
  are busy. Use error/latency signals too. No universal safe RPS can be derived
187
298
  from the route count or these defaults alone. See [resilience](RESILIENCE.md).
188
299
 
189
- ## Optional stored-link capacity
190
-
191
- Each SQLite store defaults to two read-only worker connections; writable stores
192
- add one writer. Independent read/write admission caps default to 32 each across
193
- their pool. Readers are configurable from 1–8, and caps from 1–32. Operations have
194
- a 5-second deadline and one-second SQLite lock wait. Public serving has no writer. The
195
- initial cap is 100,000 stored records across collections; this is separate from
196
- the YAML route count. No lookup cache is used, so visibility does not depend on
197
- cache invalidation. Store failures/overload return 503 and failed workers need
198
- reload/restart; healthy readers can continue while readiness is degraded. Management has a separate listener with 64 connections, 8 KiB
199
- headers and 16 KiB JSON bodies. Rate limiting remains an ingress responsibility.
200
- Do not extrapolate in-memory redirect benchmark numbers to database lookups;
201
- measure disk, writes, contention and restoration on the target host.
202
-
203
300
  Configuration parsing/schema validation now run in a terminated-on-deadline worker;
204
301
  route compilation still runs cooperatively on the host (10 seconds, yields every
205
302
  64 routes). Source, AST, structured-clone output, compiled routes, assets, module
@@ -0,0 +1,97 @@
1
+ # CI and release follow-up, September 19, 2026
2
+
3
+ This is the evidence and disposition for [#185](https://github.com/jimhoyd-com/urlcode/issues/185),
4
+ after the Windows fixes (#190) and matrix change (#192). It does not authorize
5
+ publication or change required checks.
6
+
7
+ ## Reproducible measurements
8
+
9
+ ```sh
10
+ npm run ci:history -- 100 2026-09-19 > /tmp/ci-history.json
11
+ npm run ci:report -- 35482828280
12
+ ```
13
+
14
+ The history helper reads GitHub through `gh`, with four concurrent requests at
15
+ most. It does not dispatch new runs. Increase the limit (maximum 1,000) as more
16
+ history accumulates. Use a date cutoff after the rollout when comparing the
17
+ same workflow generation. It groups by event, inferred lane and exact matrix,
18
+ reports counts and nearest-rank p50/p95, and flags groups with fewer than 20
19
+ successful first attempts. Failures/cancellations remain visible but are excluded
20
+ from successful-run percentiles. Reruns are excluded because run creation time
21
+ includes the human delay before retrying. Earlier attempts are not a complete
22
+ usage ledger. Raw job/step durations and run URLs remain in the output.
23
+
24
+ Workflow elapsed time ends at the last active job's completion. Runner-minutes
25
+ sum overlapping job execution durations: they are neither wall time nor billed
26
+ minutes. Job-creation-to-start is an observed scheduling interval, not a guarantee
27
+ of pure runner queue time. Workflow-creation-to-start also includes dependencies.
28
+ Missing timestamps remain missing; they are never treated as zero.
29
+
30
+ The initial 100-run sample spans September 19 04:59 UTC through September 20
31
+ 02:00 UTC. Successful first-attempt results:
32
+
33
+ | Configuration | Samples | Wall p50 / p95 | Runner-minute p50 / p95 |
34
+ | --- | ---: | --- | --- |
35
+ | Historical combined main, 9 jobs | 20 | 8m27s / 9m04s | 49.82 / 53.62 |
36
+ | Historical combined PR, 3 jobs | 40 | 6m12s / 8m52s | 17.28 / 24.57 |
37
+ | Split full main, 18 jobs | 1 | 7m36s / 7m36s | 74.30 / 74.30 |
38
+ | Split compact main, 10 jobs | 1 | 6m48s / 6m48s | 43.20 / 43.20 |
39
+ | Split compact PR, 10 jobs | 2 | 6m38s / 7m15s | 40.23 / 43.58 |
40
+
41
+ Historical groups contain changing code and workflow revisions; they are context,
42
+ not a controlled experiment. The sample includes no classified docs-only runs.
43
+ It cannot satisfy the 20-run acceptance criterion for the new lanes. Do not
44
+ manufacture 20 redundant workflow runs to fill the sample.
45
+
46
+ The [compact main run](https://github.com/jimhoyd-com/urlcode/actions/runs/35482828280)
47
+ used 31.1 fewer runner-minutes than the preceding
48
+ [full matrix main run](https://github.com/jimhoyd-com/urlcode/actions/runs/35482515877),
49
+ an observed 41.9% reduction between two runs, not an established long-term rate.
50
+ Its Windows core job took 377 seconds: 258 in `npm test`, 58 in package smoke,
51
+ 13 in installation. Linux Node 22 spent 300 of its 355 seconds in `npm test`.
52
+ The next useful optimization target is the runtime suite, not removing static,
53
+ container, audit or provenance checks. CLI subcommands are separate steps in the
54
+ raw report. File-level profiling is still needed before choosing balanced shards;
55
+ extra shards would add setup and runner pressure.
56
+
57
+ ## Changes delivered in this follow-up
58
+
59
+ - A Windows validation failure exposed rejected auth initialization returning
60
+ before its worker terminated. The caller now awaits termination before receiving
61
+ the original error; a deterministic regression test proves the ordering.
62
+ - `ci:history` makes the remaining baseline measurable without new CI jobs.
63
+ - Auth/admin release instructions now use scoped monorepo tags and the correct
64
+ trusted-publisher workflow filenames. Package agent guides file issues here
65
+ and point documentation at root `docs/`.
66
+ - Optional evals checks its credential before checkout/setup/install, and retains
67
+ artifacts for 14 days. This avoids unused setup; it does not remove the weekly
68
+ quality check or claim measured savings from a workflow with no prior runs.
69
+ - The manual signed candidate builds all four tarballs and installs them together
70
+ in a temporary consumer outside the workspace. It verifies the peer dependency
71
+ tree, installed versions, public imports, and `init --with auth,admin,ui`.
72
+ `train.json` records package SHA-512 integrity and the source commit; the
73
+ candidate manifest/checksums and provenance include the extension archives.
74
+ Failure stops the candidate before attestation/upload. Nothing is published.
75
+
76
+ The candidate validates the proposed package set, not live-provider behavior or
77
+ registry OIDC. Per-package release workflows still prepare their own archives
78
+ and retain their original bytes for retries; candidate archives are not silently
79
+ substituted for published release bytes. Before a release, dispatch the candidate
80
+ at the selected main commit, then the full verification workflow, and inspect
81
+ both results before using the authorized coordinator.
82
+
83
+ ## Remaining decisions and external validation
84
+
85
+ | Item | Disposition |
86
+ | --- | --- |
87
+ | Windows packaging/process coverage | Delivered in #190/#192; Node 24 on relevant PRs/main, all supported versions nightly/manually. |
88
+ | 20-run baseline per new lane | Wait for organic runs, then rerun the helper with a rollout cutoff. |
89
+ | Dependency-aware package selection | Keep conservative coverage for now. UI feeds auth/admin; auth feeds admin; core extension/CLI/scaffold changes affect the full composition. A package map must include integration, generated styles, peers and tooling, not just changed directory names. |
90
+ | Changeset/no-release enforcement | Still open. Define explicit core release intent as well as workspace Changesets; require reviewed reasons for no-release cases before implementing a gate. Blanket source-path rules would misclassify tests/tooling and root core is not versioned by Changesets. |
91
+ | Move core into `packages/core` | Separate migration, not a prerequisite for fast CI. Root-relative build, package files, CLI, Docker and starter paths make this higher risk than keeping the explicit root inventory. |
92
+ | OIDC and retained-artifact retry | Needs the next explicitly authorized release. No synthetic run proves npm's trust configuration or partial publication recovery. |
93
+ | Historical GHCR image labels/digests | Blocked on read access: anonymous registry lookup returned 403; organization package API explicitly requires `read:packages`. No labels/digests were verified and no images or publication settings changed. The repository has no `PUBLISH_CONTAINER` variable; inherited organization variables could not be inspected with this credential. |
94
+ | Dependabot grouping / unchanged nightly reuse | No change yet. Measure update PR fanout before grouping; unchanged source can still acquire new advisory findings, so reusing old verification indiscriminately would hide them. |
95
+
96
+ Keep #185 open for these acceptance items. The remaining items are not evidence
97
+ that a publish or recovery rehearsal has already succeeded.
@@ -0,0 +1,322 @@
1
+ # CI and monorepo release audit — 2026-09-19
2
+
3
+ Status: historical findings and recommendation. The accompanying implementation
4
+ and current commands are documented in [the development pipeline](DEVELOPMENT-PIPELINE.md).
5
+ The proposed helper table below records the audit design, not the installed command list. Source inspected:
6
+ `3a3d9adf6f0ec53faa078c93dfb977aa5ac416f7`; GitHub settings, runs, tags,
7
+ releases and npm dist-tags were read on September 19. Concurrent PRs may change
8
+ this snapshot. This review does not authorize publishing or changing protections.
9
+
10
+ ## Recommendation
11
+
12
+ Keep npm workspaces and Changesets, independent package versions, and the
13
+ existing public package names. Add an explicit change classifier for CI and one
14
+ release coordinator for all four packages. Preserve the existing required gate
15
+ and CodeQL policy. Stop using tag pushes as the place to discover whether a
16
+ release is buildable. Prepare and validate an immutable release plan first;
17
+ create tags from that plan once; resume partial publication without moving them.
18
+
19
+ Do this incrementally. A repository layout migration, Nx, Turborepo, remote
20
+ caching, or fewer security assertions is not a prerequisite for faster PRs.
21
+
22
+ ## Measured bottleneck
23
+
24
+ [PR #181](https://github.com/jimhoyd-com/urlcode/pull/181) changed only
25
+ `AGENTS.md` (9 additions, 4 deletions). Its
26
+ [verify run](https://github.com/jimhoyd-com/urlcode/actions/runs/35475753912)
27
+ took **9m08s** from creation to completion.
28
+
29
+ | Job | Execution time |
30
+ | --- | ---: |
31
+ | verify, Linux / Node 22 | 8m24s, after 39s from workflow creation to job start |
32
+ | verify, Linux / Node 24 | 5m49s |
33
+ | verify, Linux / Node 26 | 5m36s |
34
+ | build-fidelity | 27s |
35
+ | action | 25s |
36
+ | container | 22s |
37
+ | audit | 19s |
38
+ | verify-complete | 2s |
39
+
40
+ Within Node 22, `npm run verify` took 7m33s. Its core tests took 281.7s,
41
+ auth tests 92.9s, admin tests 23.6s, and UI tests 2.7s. These suites run
42
+ **serially**. Lint, typecheck and source checks took roughly 29s combined.
43
+ Package verification subsequently took 28s; drills took 7s. This is chiefly
44
+ execution time, not queueing, in this example.
45
+
46
+ The same pattern appears in
47
+ [PR #180's run](https://github.com/jimhoyd-com/urlcode/actions/runs/35475563183):
48
+ 8m38s overall, with Node 22 the longest job. These are sampled runs, not a
49
+ long-term percentile study or a demonstrated performance gain.
50
+
51
+ Conversely, the
52
+ [UI alpha.6 release](https://github.com/jimhoyd-com/urlcode/actions/runs/35475220590)
53
+ took 19m23s: approximately 11m24s before the job started, then 7m37s in the
54
+ whole-repository verify step. Both queue pressure and unnecessarily broad
55
+ release verification matter there; the API does not establish the queue's cause.
56
+
57
+ The workflow already avoids branch-push plus PR duplication and cancels stale
58
+ PR runs. Its three PR matrix legs repeat lint, types, generated-file checks,
59
+ builds, every package's tests, package smoke tests and drills. Main expands this
60
+ to nine OS/Node combinations. `test:package` builds again after `verify` has
61
+ built; candidate/core release explicitly build before calling `verify`, which
62
+ builds again, and then `test:package`, which builds a third time.
63
+
64
+ ## What the checks buy
65
+
66
+ | Check | Purpose | Proposed placement |
67
+ | --- | --- | --- |
68
+ | Documentation and generated resources | Prevent stale references, guidance and authoring resources | Every PR; standalone fast lane for prose changes |
69
+ | Lint and typecheck | Source and contract errors | Once per relevant change on a canonical Node version |
70
+ | Core and extension regressions | Behavioral, integration and isolation invariants | Affected packages and downstream consumers on PRs; full validation for releases |
71
+ | Supported Node versions | Detect runtime compatibility failures | Retain all three versions for runtime changes initially; avoid repeating static checks |
72
+ | Windows/macOS | Path, process, filesystem and platform differences | Targeted PR coverage plus full main coverage initially |
73
+ | Package install smoke | Prove shipped archives and declarations work | Relevant package/CLI/starter changes and releases; lightweight pack inventory for shipped-doc changes |
74
+ | Container | Validate the shipped execution environment | Keep existing required job; early optimization can leave this cheap job alone |
75
+ | Action smoke | Exercise the consumer-facing composite action | Runtime/action/starter/build changes |
76
+ | Build fidelity | Detect nondeterministic emitted files and packs | Build/package/toolchain changes and releases |
77
+ | Runtime dependency audit | Detect known advisories | Keep initially: measured cost is small; also run on a schedule and before release |
78
+ | CodeQL | Static security analysis | Preserve current enforced policy; not the observed critical path |
79
+ | Operational drills | Local lifecycle/recovery regression signal | Relevant runtime changes, main and release validation |
80
+ | Real-model evals | Authoring quality regression | Existing weekly/manual workflow; no need to put it on ordinary PRs |
81
+
82
+ Do not remove timeout, worker replacement, sandbox or authentication tests to
83
+ save time. Profile and change their scheduling or fixtures while preserving the
84
+ behavior they establish. The longest sampled core subtests included explain
85
+ versus runtime agreement (~35s), a slim-image installer test (~21s), and sitemap
86
+ limits (~21s). Investigate repeated CLI startups and repeated project loading;
87
+ these measurements do not yet establish which internal operation dominates.
88
+ Auth's ~93s warrants separate profiling, including password hashing, without
89
+ changing production security parameters to speed up tests.
90
+
91
+ ## PR workflow shape
92
+
93
+ Always start the workflow. A small, tested classifier compares the complete PR
94
+ diff against its base, including renamed/deleted paths, and emits an explicit
95
+ job plan. Missing history, unknown paths, classifier errors and workflow/shared
96
+ toolchain changes select full verification. Do not infer safety just from a
97
+ `.md` extension: starter, recipe and executable authoring inputs need their
98
+ own categories. Core changes affect all consumers; UI affects auth/admin; auth
99
+ affects admin; admin-only changes need not retest all of core.
100
+
101
+ For a prose-only PR, run guidance and generated-resource checks, local link and
102
+ reference checks, and applicable package file-inclusion assertions. Extract this
103
+ from today's `check` rather than calling the full source/test syntax walk.
104
+ Changes to generator code, manifests, lockfiles, schemas, executable examples or
105
+ CI configuration must leave the prose-only lane.
106
+
107
+ For code PRs, run static checks once, build required outputs/styles in dependency
108
+ order, and schedule core/UI/auth/admin tests independently. Start by preserving
109
+ the current Node coverage. Pilot two balanced core-test shards on the slowest
110
+ leg, measure runner-minutes and wall time, and increase only if the gains justify
111
+ the extra jobs. Keep the full local `npm run verify` entry point.
112
+
113
+ The `verify-complete` gate must know which jobs the classifier required. It must
114
+ reject failure, cancellation, missing results and unexpected skips; accept a
115
+ skip only when the validated plan explicitly marks that job unnecessary. Test
116
+ the gate's failure paths. The present gate rejects every skipped dependency, so
117
+ adding `if:` conditions without updating its contract will break merging.
118
+
119
+ Do not add workflow-level `paths-ignore` to a required workflow: GitHub documents
120
+ that such skipped workflows can leave required checks pending. Use job selection
121
+ inside an always-triggered workflow instead.
122
+ [GitHub documentation](https://docs.github.com/en/actions/how-tos/manage-workflow-runs/skip-workflow-runs)
123
+
124
+ The live main ruleset requires `container` and `verify-complete`, and separately
125
+ enforces CodeQL findings. It has no bypass actors. Its strict up-to-date setting
126
+ is **false**, contrary to CONTRIBUTING's claim that an up-to-date branch is
127
+ required. Preserve protections in the first implementation; reconcile that
128
+ documentation. A later policy change needs a deliberate review, not an implicit
129
+ side effect of renaming jobs.
130
+
131
+ Windows failures are real: the
132
+ [main run at cc582f2](https://github.com/jimhoyd-com/urlcode/actions/runs/35475923267)
133
+ failed on `spawnSync npm.cmd EINVAL` in UI packaging. PR
134
+ [#182](https://github.com/jimhoyd-com/urlcode/pull/182) already addresses this.
135
+ Linux-only PR tests let this reach main. Add a focused Windows packaging/process
136
+ smoke for relevant changes before considering a smaller main matrix. Keep the
137
+ existing full main matrix during rollout; moving exhaustive coverage to nightly
138
+ is a later tradeoff, and release candidates must still pass full validation.
139
+
140
+ Initial goals: prose PRs under 90 seconds excluding runner queueing; ordinary
141
+ code PRs under 5 minutes. These are targets, not measured promises. Report queue
142
+ time separately from execution, and compare p50/p95 over at least 20 runs per
143
+ change class before deciding the optimization succeeded.
144
+
145
+ ## Release and tag findings
146
+
147
+ 1. **Tags have actually moved across source revisions.** Release history for
148
+ `v0.4.0-alpha.1` records five distinct commits: `b5cd619`, `871dd87`,
149
+ `bb0f9e6`, `84e45ea`, then successful `8dabc7e`. The sampled history also
150
+ shows multiple commits for `v0.3.0`, `v0.2.0`, and `v0.1.0`. This establishes
151
+ reuse of tag names across commits, not who changed them or whether every
152
+ failed attempt published an artifact. The live ruleset inventory contained
153
+ only a branch ruleset, not tag protection.
154
+ 2. **An ancestor of main is not necessarily a validated release commit.** All
155
+ four release workflows check main ancestry, but do not require the exact
156
+ commit's main CI result. UI alpha.6 published from `8fa7f8b` while that
157
+ commit's main verify run failed on Windows. Release validation was Linux-only.
158
+ 3. **Core alphas are classified as normal GitHub releases.** Both alpha.1 and
159
+ alpha.2 had `prerelease: false`; GitHub's latest-release endpoint returned
160
+ alpha.2. Core's release creation omits the prerelease flag. npm correctly
161
+ separates its `alpha` channel; this is a distinct GitHub-channel defect.
162
+ 4. **Core container publication always updates `latest`.** `release.yml` tags
163
+ and pushes `latest` even for prereleases when `PUBLISH_CONTAINER` is enabled.
164
+ The code path is confirmed; this audit did not establish whether an alpha
165
+ actually overwrote the live GHCR tag.
166
+ 5. **Candidate and release have already diverged.** Candidate reads Dockerfile
167
+ into two shell variables, swallowing `AS build` into the image variable and
168
+ failing the digest regex. The same parse was fixed only in release. Running
169
+ candidate's parser locally against the current Dockerfile reproduced failure.
170
+ The last listed candidate successes predate this snapshot; no new candidate
171
+ was dispatched during this audit. Existing release tests check release's parser,
172
+ not both paths.
173
+ 6. **Existence is treated as sufficient for retry.** Publish steps skip an
174
+ existing npm version without checking its integrity against the candidate;
175
+ GitHub release uploads use `--clobber`. The fidelity job tests repeated packing
176
+ in one checkout, not equality to a prior publication. Extension builds use
177
+ floating Node 22 and registry-installed peers, so same-commit reproducibility
178
+ across days is not established. Preserve and compare the original artifact.
179
+ 7. **Concurrency is scoped to a tag.** Two different versions of one package can
180
+ publish concurrently; there is no shared release-train order or monotonic
181
+ channel guard. A late old release could update a channel after a newer one.
182
+ 8. **Changesets is only partially in charge.** Version 3.0.3 is installed and
183
+ pre-mode is enabled, with independent versions. Core is outside its workspace
184
+ package set. The tag checker checks trigger disjointness, not remote tag SHA,
185
+ registry integrity, missing changesets, peer compatibility or channel state.
186
+ PR [#183](https://github.com/jimhoyd-com/urlcode/pull/183) already tackles a
187
+ Changesets peer-range rewrite and stale channel documentation; build on it.
188
+ 9. **Release documentation contains competing snapshots.** VERSION-ALIGNMENT
189
+ still describes old repository pins, and the Changesets README says only UI
190
+ is covered and publishing is not wired up, despite a successful monorepo UI
191
+ release. RELEASE-SECURITY describes candidate/release as sharing a path that
192
+ has demonstrably diverged. Generate the live inventory; retain policy in prose.
193
+
194
+ The actual npm channels at audit time were:
195
+
196
+ | Package | `latest` | `alpha` |
197
+ | --- | --- | --- |
198
+ | core | 0.3.0 | 0.4.0-alpha.2 |
199
+ | ui | 0.1.0-alpha.5 | 0.1.0-alpha.6 |
200
+ | auth | 0.1.0-alpha.3 | 0.1.0-alpha.3 |
201
+ | admin | 0.1.0-alpha.3 | 0.1.0-alpha.3 |
202
+
203
+ Different channel values are not inherently drift. Historical extension `latest`
204
+ values are already alphas; do not silently move them. Define channel intent,
205
+ test the documented install combinations against peer ranges, and report
206
+ deviations. An old core release tag also should not follow current main:
207
+ unreleased development is normal; silently relabeling a published version is not.
208
+
209
+ ## One release process for four independently versioned packages
210
+
211
+ Use Changesets for reviewed release intent and changelogs, with `fixed` and
212
+ `linked` remaining empty. Require a changeset or a reviewed no-release reason
213
+ for changes to publishable behavior. A routine feature PR need not manually
214
+ bump versions: an accumulated release PR owns version changes, lockfile updates,
215
+ peer-range changes and changelogs together. This replaces VERSION-ALIGNMENT's
216
+ instruction to bump on every source-changing PR once the new flow is implemented.
217
+ [Changesets configuration](https://github.com/changesets/changesets/blob/main/docs/config-file-options.md)
218
+
219
+ Near term, keep root core where it is and include it explicitly in the common
220
+ release inventory and plan, with its existing version update reviewed in the
221
+ same release PR. Do not pretend `changeset version` covers it. Longer term,
222
+ evaluate moving core to `packages/core` with a private tooling root so Changesets
223
+ can own all four uniformly. That is a separate migration with CLI/container/
224
+ starter/package tests, not part of the urgent CI fix. Preserve existing tag
225
+ formats during this transition; any future new core tag format needs an explicit
226
+ compatibility decision for install.sh, Homebrew, action consumers and old links.
227
+
228
+ Proposed lifecycle:
229
+
230
+ 1. A protected release PR produces versions and a plan listing package name,
231
+ directory, version, tag, channel and dependency order. Manifests supply the
232
+ versions; the plan does not become a second hand-maintained version database.
233
+ 2. After merge, select and freeze its exact main SHA. Require that SHA's main
234
+ checks and release-specific validation; never substitute the newest passing
235
+ run from another commit. A canceled/missing full check must be run for the
236
+ selected SHA. Run without publication credentials during preparation.
237
+ 3. Build once with locked tooling, pack the proposed packages, and install the
238
+ actual tarballs together in a clean consumer project without development
239
+ export conditions. Separately test declared published peer floors. Where a
240
+ new floor belongs to this release, test its candidate tarball before publishing
241
+ and confirm registry resolution after its predecessor publishes.
242
+ 4. Sign and retain artifacts and a manifest binding source SHA, package versions,
243
+ dependency versions, lock hash and artifact hashes. Exercise this same prepare
244
+ path for a manual candidate; candidate must not be a second copied implementation.
245
+ 5. Create each expected tag once at that SHA. Refuse a remote tag at a different
246
+ SHA. Publish the prepared bytes in dependency order: core/UI before auth, auth
247
+ before admin, including only packages that need a release. Serialize publication
248
+ across the release train. Keep write/OIDC permissions confined to publication.
249
+ 6. On retry, reconcile each artifact with npm integrity, tags, GitHub assets and
250
+ container digests. Identical means complete; a mismatch stops. A registry
251
+ timeout is not proof a version is absent. Never clobber a different asset or
252
+ move a version tag. Reuse retained bytes; if unavailable, require a rebuild
253
+ that matches recorded hashes. A source fix needs a new version and release PR.
254
+ 7. Mark prereleases consistently on GitHub/npm/GHCR, and update mutable channels
255
+ only according to explicit policy with a guard against regressions. Summarize
256
+ partial success clearly; separate registries cannot form an atomic transaction.
257
+
258
+ Protect release tag namespaces against update/deletion, with a narrowly defined
259
+ creation path. This is a proposed strengthening, not a settings change made by
260
+ the audit. Avoid a token-created-tag event chain: coordinate preparation and
261
+ publication explicitly in workflow jobs/dispatch, rather than depending on
262
+ another push workflow being triggered by a workflow-created tag.
263
+
264
+ Initially retain the existing four trusted-publisher workflow filenames as thin
265
+ wrappers around shared checked-in helpers. Consolidating to a new filename is a
266
+ registry trust migration: verify each package's npm publisher identity and direct
267
+ publish permission first. A dry run cannot prove the OIDC exchange works.
268
+ [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/)
269
+
270
+ ## Small helper surface
271
+
272
+ These names describe proposed commands, not commands implemented by this audit.
273
+
274
+ | Command | Contract |
275
+ | --- | --- |
276
+ | `npm run ci:plan -- --base SHA --head SHA` | Print change class, affected package closure, required jobs and why; deterministic and fail closed |
277
+ | `npm run ci:report -- --run ID` | Read GitHub job/step timings, separate queue and execution, show failed checks and run links |
278
+ | `npm run release:status` | Read manifests, Changesets state, Git tags, GitHub releases, npm versions/channels/integrity and peer compatibility; no mutations |
279
+ | `npm run release:plan -- --sha SHA` | Produce the exact package/version/tag/channel/order plan, including root core |
280
+ | `npm run release:prepare -- --plan FILE` | Validate, build, pack, smoke-test and write immutable artifact manifest; no publication |
281
+ | `npm run release:publish -- --plan FILE` | CI-only mutation path; validate commit/checks/tags/artifacts, publish or resume idempotently |
282
+
283
+ Use structured JSON outputs plus a short human summary. Share the version/tag/
284
+ channel parser, manifest inventory, Docker image parser and integrity comparison
285
+ between candidate and release. Test those contracts by execution, not only by
286
+ regex checks that particular shell snippets exist in YAML. Add workflow syntax
287
+ validation and fixtures for wrong tag, wrong SHA, mismatched version, prerelease,
288
+ partial publish, existing unequal artifact, registry outage and peer-floor failure.
289
+
290
+ ## Rollout order and acceptance
291
+
292
+ 1. **Release correctness:** shared candidate parser, correct GitHub/GHCR alpha
293
+ handling, read-only status/preflight, immutable retry checks and exact-SHA
294
+ main-CI gate. Coordinate with #183; preserve the current publisher identities.
295
+ 2. **Immediate PR speed:** prose lane and tested classifier/gate. Keep existing
296
+ required check names, audit, CodeQL and container. Demonstrate a docs-only PR
297
+ finishing quickly and a deliberately failed required job blocking the gate.
298
+ 3. **Code throughput:** split static/core/workspace checks, remove redundant
299
+ builds, profile and shard the slow suites, add targeted Windows coverage.
300
+ Verify equivalent test coverage and measure queue/cost as well as latency.
301
+ 4. **Release coordination:** one Changesets release PR and package inventory,
302
+ one preparation path, serialized dependency-aware publication and immutable
303
+ reconciliation. Prove partial-failure recovery on a future authorized release.
304
+ 5. **Optional structural work:** core workspace migration and reconsideration of
305
+ the nine-leg main matrix after measured coverage/performance evidence.
306
+
307
+ This audit made no release, tag, registry-channel or branch-protection changes.
308
+ It inspected live evidence and reproduced the candidate parser failure locally;
309
+ it did not execute a new full matrix, deploy containers, change npm trust, or
310
+ prove end-to-end recovery. Passing CI is not an independent security assessment.
311
+
312
+ ## Implementation follow-through
313
+
314
+ The user subsequently authorized implementation. The accompanying PR installs
315
+ the conservative prose lane, separate core/workspace jobs, shared release
316
+ helpers, live inventory and an explicit sequential tag coordinator. Root core
317
+ remains outside Changesets; whole-train preparation before any tag and finer
318
+ package selection are tracked in [#185](https://github.com/jimhoyd-com/urlcode/issues/185).
319
+ The user explicitly approved the immutable-tag rule, now active as
320
+ [23712319](https://github.com/jimhoyd-com/urlcode/rules/23712319), without bypass.
321
+ The two historical core alpha release flags were corrected and GitHub latest
322
+ restored to stable v0.3.0. No tags or artifact bytes were rewritten.