@jimhoyd/urlcode 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -82,7 +82,8 @@ and current sample sizes.
82
82
  Core remains at the repository root. Independent extension versions remain
83
83
  supported; a coordinated version is an explicit maintainer choice, not a
84
84
  permanent fixed-version policy. An explicitly selected stable version exits
85
- Changesets alpha pre-mode; subsequent stable patches stay out of pre-mode. Feature PRs record workspace release intent in
85
+ Changesets alpha pre-mode when no package remains on alpha; subsequent stable
86
+ patches stay out of pre-mode. Feature PRs record workspace release intent in
86
87
  Changesets; core release notes remain an explicit maintainer responsibility.
87
88
 
88
89
  `release:check` checks manifest/lock versions and peer ranges, CLI and MCP
@@ -101,12 +102,41 @@ npm run release:prepare -- --version 0.4.1 --consume-changesets --execute
101
102
 
102
103
  An optional `--notes PATH` adds reviewed maintainer notes. Dry runs do not change
103
104
  files. Preparation rejects downgrades, reused local tags, dirty checkouts and
104
- stale plans. A stable target removes `.changeset/pre.json`, publishes to npm
105
- `latest`, and leaves the historical `alpha` pointer unchanged. Alpha targets
105
+ stale plans. A stable target removes `.changeset/pre.json` once no package
106
+ remains on alpha, publishes to npm `latest`, and leaves the historical `alpha`
107
+ pointer unchanged. Alpha targets
106
108
  require existing alpha mode; the helper never silently re-enters prerelease mode.
107
109
  It does not invoke a permanent Changesets fixed-version policy.
108
110
 
109
- ## One-command release and resume
111
+ ## GitHub Actions release buttons
112
+
113
+ The Actions page exposes `release core`, `release UI`, `release auth`, `release
114
+ admin` and `release all packages`. Each manual workflow accepts an exact stable
115
+ or alpha version and whether to consume relevant pending Changesets. The four
116
+ package workflows update and publish only their selected package. `release all
117
+ packages` aligns and publishes the complete train in core → UI → auth → admin
118
+ order. A Changeset spanning selected and unselected packages is rejected; use
119
+ the all-packages workflow or split the change intentionally.
120
+
121
+ All five buttons call the same serialized reusable workflow. It creates a
122
+ release PR, waits for normal required checks, merges without bypass, runs the
123
+ exact-commit full matrix and signed candidate, publishes the selected immutable
124
+ tag, checks registry installability, and verifies the current four-package
125
+ consumer combination. A core release also updates the standalone starter. The
126
+ workflow is resumable: rerun the same button and version after repairing a
127
+ failure. It reuses matching PRs, tags, candidates and successful publishers.
128
+
129
+ Configure `RELEASE_AUTOMATION_TOKEN` as a repository Actions secret. Prefer a
130
+ repository-scoped GitHub App token when available. A fine-grained PAT is also
131
+ supported when it is limited to `urlcode` and `urlcode-template` with Contents,
132
+ Pull requests and Actions read/write. The repositories are public, so the
133
+ coordinator can inspect their check runs without an additional token
134
+ permission. The token owner needs ordinary write access. Do not grant ruleset
135
+ bypass, administration, approval or package-registry credentials; npm
136
+ publishers continue to use their workflow OIDC identities. Dispatch from
137
+ `main`.
138
+
139
+ ## One-command local release and resume
110
140
 
111
141
  Inspect without writing:
112
142
 
@@ -115,12 +145,14 @@ npm run release:status # registry channels, peer compatibility, tag SHAs
115
145
  npm run release:plan # manifest-derived inventory
116
146
  npm run release:run # ordered states at this checkout: pending/resume/unchanged
117
147
  npm run release:run -- --version 0.4.1 --consume-changesets
148
+ npm run release:run -- --version 0.4.2 --package auth --consume-changesets
118
149
  ```
119
150
 
120
151
  For an explicitly authorized coordinated release:
121
152
 
122
153
  ```sh
123
154
  npm run release:run -- --version 0.4.1 --consume-changesets --execute
155
+ npm run release:run -- --version 0.4.2 --package auth --consume-changesets --execute
124
156
  ```
125
157
 
126
158
  `--execute` authorizes the entire sequence: create the release branch/PR, wait
@@ -241,7 +273,9 @@ npm uses OIDC with pinned npm 11.5.1. Alpha versions use npm/GHCR `alpha`, and
241
273
  GitHub prerelease classification with `--latest=false`. Existing `latest`
242
274
  pointers are not promoted by this flow. Core GHCR publication remains conditional
243
275
  on `PUBLISH_CONTAINER=true`; its existing version/channel identity guards remain.
244
- Historical GHCR verification is still a separate follow-up.
276
+ Historical GHCR verification is still a separate follow-up. Digest promotion of a
277
+ tested image is design only, in [CONTAINER-PROMOTION.md](CONTAINER-PROMOTION.md);
278
+ the publisher still builds from source.
245
279
 
246
280
  The [Immutable release tags rule](https://github.com/jimhoyd-com/urlcode/rules/23712319)
247
281
  blocks updates/deletions of `v*` and `@jimhoyd/urlcode-*@*`, permits creation, and
@@ -183,29 +183,18 @@ Node adapter conformance is not a live-provider deployment claim.
183
183
 
184
184
  ## Project-level lifecycle hooks
185
185
 
186
- `authorize` and `middleware` let core call *into* an extension. They do not
187
- let a project hand its own code *to* an extension to run at a defined point.
188
- That gap matters once an extension has meaningful lifecycle events
189
- registration, deletion, an administrative action, a link resolution, and so
190
- on. Presentation already has a standard layering mechanism for this: a
191
- project customizes an extension's *look* through `urlcode-ui`'s
192
- `copy`/`extra.css`/`templates` without forking it. Behavior needs the same
193
- standard, or every extension author (and every project depending on one)
194
- either reinvents it or forks the extension. `urlcode-auth` and
195
- `urlcode-admin` independently hit this gap (auth/#35, admin/#32); auth's own
196
- [SPIKE-AUTH.md](../packages/auth/docs/SPIKE-AUTH.md)
197
- already scoped a shape for `onSignUp`/`beforeRegister`/`onDelete`.
198
-
199
- **The pattern.** An extension with lifecycle events an author judges worth
200
- exposing should let the project name its own function in the extension's own
201
- `config`, using the same source shape `function`/`middleware` routes already
202
- use (a string path, or `{source, export, args}` — `schemas/urlcode.schema.json`),
203
- and add its own `sandbox` boolean next to it (below). The extension's own
204
- `activate()` reads that config, and its own runtime dispatch — not a new core
205
- primitive, an ordinary call the extension package makes with the request
206
- context it already has — invokes the named function at the lifecycle point
207
- it defines, with a typed input and a typed verdict the extension's own
208
- schema documents. For example, an auth-style extension might declare:
186
+ Extensions expose project customization points through the core hook primitive.
187
+ Each registration publishes `hooks`, a machine-readable list containing the
188
+ hook name, whether it is a value-transforming `filter` or side-effect `action`,
189
+ its description and its input/output JSON Schemas. The extension embeds
190
+ `extensionHooksSchema(contracts)` in its configuration schema and calls
191
+ `loadExtensionHooks(config.hooks, contracts, context)` during activation.
192
+ Core then enforces the common source/export shape, project-root confinement,
193
+ known names, eager module/export validation, input/output schemas and reload
194
+ cache busting. Hook entry bytes participate in the project revision, so editing
195
+ a hook invalidates the operator's extension pin.
196
+
197
+ Projects select those declared hooks in the extension's own configuration:
209
198
 
210
199
  ```yaml
211
200
  extensions:
@@ -221,76 +210,51 @@ extensions:
221
210
  ```
222
211
 
223
212
  with `beforeRegister` called before an account is created, given a typed
224
- `{email, traits}`-shaped input and returning a typed verdict (`{allow: true}`
213
+ `{email, profile?}` input and returning a typed verdict (`{allow: true}`
225
214
  or `{allow: false, reason}`), and `onSignUp` called after, for side effects
226
- such as provisioning a workspace. The exact hook names, input/verdict shapes
227
- and invocation points are the extension's own design `hooks` is not a core
228
- schema key — but the source/export/sandbox shape, and the fact that this is
229
- project code the extension calls rather than an operator callback in
230
- `host.mjs`, should be consistent across extensions so an author who has
231
- learned one has learned the pattern.
232
-
233
- **Trust and execution mode.** Project-level lifecycle hooks are first-party
234
- project code, the same category as any `function`/`middleware` route, and
235
- follow the same trusted-by-default rule with no special case
236
- (docs/SPIKE-DEFAULT-TRUST-MODEL.md, [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md)):
237
- trusted, in-process execution by default; a project sets `sandbox: true` on
238
- a given hook to opt that hook into isolated QuickJS/WASM execution, exactly
239
- the mechanism `function`/`middleware` routes already use and for the same
240
- reason the hook's own code, not the trustworthiness of whatever triggered
241
- it, is what calls for isolation (docs/AI-AUTHORING.md's "Deciding when a
242
- route needs `sandbox: true`"). This was raised as an open question — whether
243
- a lifecycle hook should always run sandboxed because it makes a
244
- security-relevant decision and settled the other way: uniformity with the
245
- rest of the trust model was chosen over hardwiring isolation for lifecycle
246
- hooks specifically, the same "no special case" call already made between
247
- `function` and `middleware` trust. A `beforeRegister` hook enforcing "only
248
- `@acme.com` may register" is the project's own governance rule over its own
249
- signup flow; it is not more dangerous than any other route the project
250
- wrote, and does not get a different default.
251
-
252
- Core's own trusted/sandboxed dispatch (`TrustedFunctions`/`FunctionPool`,
253
- `src/runtime.ts`) is wired to route dispatch, not exposed to extensions — but
254
- each half of a hook's `sandbox: true` opt-in has its own answer:
255
-
256
- - **Trusted (the default, no `sandbox: true`).** No core primitive is needed
257
- or provided: this is ordinary first-party project code, and the
258
- extension's own `activate()` already has `ExtensionActivation.root` to
259
- resolve the hook's `source` against and can `import()` it directly, the
260
- same way any trusted `function`/`middleware` route does. Do that import
261
- with a per-activation cache-busting query, the way core's own trusted
262
- route activation does (`src/trusted-functions.ts`): Node's ESM loader
263
- caches a resolved module forever by URL, so a plain `import()` of the
264
- unchanged file URL makes a second activation in the same process keep
265
- serving the hook code that was on disk at the first one
266
- (jimhoyd-com/urlcode#198). Only the hook's **entry** module is refreshed
267
- this way — modules the hook itself imports stay on Node's module cache,
268
- the same limitation the trusted route path has, so a change to a hook's
269
- own dependency still needs a process restart.
270
- - **Sandboxed (`sandbox: true`).** `@jimhoyd/urlcode/sandbox` exports
271
- `SandboxPool`, the same QuickJS/worker-thread engine that backs a
272
- sandboxed `function`/`middleware` route — the identical module-allowlist
273
- walk, memory/stack limits, two-layer deadline enforcement, `maxBytes` and
274
- response-shape validation, with no separate or weaker engine for
275
- extensions. It takes an explicit list of `{source, export}` entries
276
- (resolve a hook's `source` string with the re-exported `functionFile()`,
277
- the same resolution/validation a native route's `source` gets) instead of
278
- anything route/YAML-shaped, and `execute({entry, chain}, request, context)`
279
- in place of a `FunctionRoute`. There is no "trusted" mode exported
280
- alongside it — `SandboxPool` is only ever the isolated path; see
281
- [FUNCTION-SECURITY.md](FUNCTION-SECURITY.md) and
282
- [TYPESCRIPT.md](TYPESCRIPT.md) for the full contract.
283
-
284
- An extension honoring a hook's `sandbox: true` is expected to actually
285
- isolate that invocation through `SandboxPool` now that the primitive exists
286
- (or document plainly that it does not yet, rather than accepting the field
287
- and silently running it trusted) — say which, in the extension's own docs,
288
- so an author reading them is not misled about what opt-in exists.
215
+ such as provisioning a workspace. Hook names and lifecycle timing remain the
216
+ extension's domain, while their declaration, loading and discovery are shared.
217
+
218
+ Hooks are first-party project code and run trusted in-process by default, with
219
+ full Node access, like trusted `function` and `middleware` routes. Contract v1
220
+ does not define an arbitrary-value sandbox hook protocol. A hook reference with
221
+ `sandbox: true` is rejected during activation rather than silently run trusted.
222
+ Only the entry module is refreshed during reactivation; its imported dependencies
223
+ remain in Node's module cache until restart.
224
+
225
+ The UI extension exposes `transformView`, a synchronous filter called before a
226
+ named kit template renders. It receives `{template, view}` and returns the view
227
+ model to render. Use copy, templates, theme and CSS for ordinary presentation
228
+ changes; use this hook for project-specific computed view data that those
229
+ declarative layers cannot express.
230
+
231
+ ## Building an extension
232
+
233
+ An extension package should export a registration factory and, when it supports
234
+ `urlcode init --with`, a side-effect-free `scaffold` function. The registration:
235
+
236
+ 1. Declares its logical name, contract version, supported targets, exact project
237
+ revision pin and strict configuration/policy schemas.
238
+ 2. Publishes every project hook through `hooks` and reuses
239
+ `extensionHooksSchema` plus `loadExtensionHooks`; it does not implement its
240
+ own path resolver or dynamic-import cache.
241
+ 3. Activates all configuration, files, services and hooks before serving a
242
+ request. Invalid or stale configuration fails activation.
243
+ 4. Returns `handle` for mounts and optionally `authorize`/`middleware` for route
244
+ policies. It closes resources it owns.
245
+ 5. Keeps credentials, storage and provider setup in the operator host. Project
246
+ YAML contains logical configuration and project-relative hook references.
247
+
248
+ Consumers install the package, declare its YAML block and mounts/policies, and
249
+ register it in `host.mjs`. They modify it through declared configuration,
250
+ presentation layers and hooks. A fork is reserved for changing behavior the
251
+ extension has not exposed; that is evidence for a new declarative field or hook.
252
+ See [Composing a site](COMPOSING-A-SITE.md) for the complete ui/auth/admin example.
289
253
 
290
254
  ## Discovering schemas
291
255
 
292
256
  Each registration carries the JSON Schemas that validate its `config` block and
293
- its per-route policy requirements. `urlcode extensions` prints them together with
257
+ its per-route policy requirements, plus its hook contracts. `urlcode extensions` prints them together with
294
258
  the project's own declarations so an author can see what a mount accepts:
295
259
 
296
260
  ```sh
@@ -299,6 +263,7 @@ urlcode extensions --project ./site --host-file /absolute/operator/host.mjs [--j
299
263
 
300
264
  For every registration in the host file it reports the name, contract version,
301
265
  targets, credential headers, configuration schema, policy schema (if any),
266
+ declared hook names, kinds, descriptions and input/output schemas,
302
267
  whether the project declares it, whether its `projectSha256` matches the current
303
268
  revision, the routes that mount it and the routes whose policies require it.
304
269
  Declared names the host does not register are listed as unregistered. The command
@@ -93,14 +93,11 @@ machinery either way.
93
93
 
94
94
  This engine — worker spawning, the module-allowlist walk, the two-layer
95
95
  deadline, `maxBytes` and response-shape validation — is one implementation
96
- shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`,
97
- the public primitive an extension package uses to run a project-supplied hook
98
- through this exact isolation when the project's own config declares
99
- `sandbox: true` on it (see [EXTENSIONS.md](EXTENSIONS.md#project-level-lifecycle-hooks)
100
- and [TYPESCRIPT.md](TYPESCRIPT.md)). Every guarantee above applies identically
101
- through that entry point; there is no separate, weaker sandbox for extensions
102
- to reach for, and no "trusted" mode exported there — trusted execution needs
103
- no primitive at all (docs/SPIKE-DEFAULT-TRUST-MODEL.md).
96
+ shared by route dispatch and by `@jimhoyd/urlcode/sandbox`'s `SandboxPool`.
97
+ That public HTTP-shaped primitive remains available to extension authors, but
98
+ project extension hooks use arbitrary typed values and contract v1 runs them
99
+ trusted in-process; it rejects `sandbox: true` rather than claiming HTTP sandbox
100
+ semantics apply to them. See [extensions](EXTENSIONS.md#project-level-lifecycle-hooks).
104
101
 
105
102
  ## What the trusted default can and can't do
106
103
 
@@ -1,6 +1,7 @@
1
1
  # Decisions to align
2
2
 
3
- Reviewed 2026-09-19 against core `db375bf` and the current public package sources.
3
+ Last reconciled 2026-09-20 against open and closed issues (#242); the source
4
+ review it began from is dated 2026-09-19 (core `db375bf`, now archived).
4
5
  This is the maintainer's decision list, not a second implementation backlog.
5
6
  The [roadmap](../ROADMAP.md) gives sequence and the [archive](archive/README.md)
6
7
  keeps earlier discussions. Recommendations below are not accepted decisions.
@@ -33,31 +34,14 @@ keeps earlier discussions. Recommendations below are not accepted decisions.
33
34
  |---|---|---|
34
35
  | Where does work status live? | Several old plans repeated issues and continued calling delivered work unfinished. | Issues for actionable status, this short roadmap for sequence, archive for completed proposals. Preserve evidence gaps when archiving. |
35
36
  | Expand into business applications now? | No collection handler or proposed business suite is implemented; the model-backed benchmark evidence is missing. | Measure existing tasks and record repeated application plumbing before selecting a collection/CMS/forms project. Retired short-link products stay retired. [Proposal](SPIKE-BUSINESS-SUITE.md). |
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
+ | Fold extension schemas into retrieved context? ([#174](https://github.com/jimhoyd-com/urlcode/issues/174), open) | `urlcode extensions` and the MCP `get_extensions` query return the registered configuration and policy schemas, but `src/context.ts:113` reports `extensions` as names only, so an author writing `extensions.<name>.config` or `policies.extensions.<name>` must run the separate operator-authorized query first. | Decide from retrieval and task evidence, not preference: the existing small-task harness can measure whether folding schemas into bounded context improves authoring. Keep the token budget bounded and never auto-load a project-selected host file. This is a discovery improvement, not a defect in the existing query. |
38
+ | Build the tested-image promotion path? ([#233](https://github.com/jimhoyd-com/urlcode/issues/233), open) | [Design and an inert invariant helper](CONTAINER-PROMOTION.md) are merged. `release.yml` still rebuilds from source when `PUBLISH_CONTAINER` is true, and a retry accepts an existing `:VERSION` on its `revision` label alone. Live GHCR state and whether GHCR preserves an OCI digest through `skopeo`/`crane` are unverified. | The reason is integrity (tested bytes are the shipped bytes), not speed: CI's image build took about 12 seconds, so a cross-run build cache is not justified and should not be added. Recommendation: do not implement while publication is off and GHCR is uninspected. First grant `read:packages` and inspect historical labels and digests; then land a candidate-side image build behind an operator input, inspect one real candidate, and only then change the publisher. Not changed here. |
37
39
  | Keep the POST-plus-`request.body` sandbox advisory? | `src/readiness.ts` nudges any code-running route that accepts POST with a declared `request.body` and declares neither `sandbox: true` nor `sandboxReason`. It is advisory only: never fails `audit`, never changes `ready`. | The nudge keys on request *shape* while [AI authoring](AI-AUTHORING.md) tells authors to decide on *code* trust, so it can read as "untrusted input implies sandbox" -- the reasoning that guidance explicitly rejects. It still has value as a prompt to record a decision. Recommendation: keep the trigger, restate the message as a request to record the trust decision (`sandbox: true` or `sandboxReason`) rather than as a suggestion that this route may need isolation. Not changed here; #196 was a docs/tooling alignment pass. |
38
40
 
39
41
  The broader [AI benchmark proposal](SPIKE-AI-FRAMEWORK-BENCHMARK.md) also needs a
40
42
  chosen application, model-run budget and execution authorization. The existing
41
43
  small-task harness can supply evidence without committing to that larger study.
42
44
 
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
45
  ## Accepted: one Node deployment per project
62
46
 
63
47
  **Decided 2026-09-19.** Projects that use `function` or `middleware` deploy as
@@ -140,85 +124,26 @@ reasoning above is the kind of prose that rots as soon as `auth` and `admin`
140
124
  arrive, which is the whole argument this repository makes for enforcing checks
141
125
  over documented intent.
142
126
 
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.
127
+ ## Completed work (archived)
128
+
129
+ Closed questions, the completed monorepo migration (ui, auth and admin are
130
+ workspace packages; released from this repository) and the 2026-09-19 source
131
+ review baseline live in
132
+ [archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md](archive/2026-09-20/OPEN-DECISIONS-COMPLETED.md).
133
+ The migration plan is [archived](archive/2026-09-19/SPIKE-MONOREPO.md). Versions
134
+ and channels are in [version alignment](VERSION-ALIGNMENT.md) and
135
+ `npm run release:status`, not here.
136
+
137
+ ## Accepted: middleware withdrawn rather than consolidated
138
+
139
+ **Decided 2026-09-19.** `@jimhoyd/urlcode-middleware` was unpublished and its
140
+ repository deleted; there is no `packages/middleware` and nothing to fold into
141
+ core afterward (#172, closed as moot). Per-route middleware is native to core
142
+ via the `middleware:` array ([MIDDLEWARE.md](MIDDLEWARE.md)). The generic
143
+ extension wrapping hook (`ExtensionInstance.middleware`,
144
+ `RuntimeExtension.cacheSensitive`) stays in core's contract for other extensions
145
+ but is no longer exercised by any shipped package, so do not assume it is
146
+ covered. Static targets keep rejecting request-time middleware. The earlier
147
+ instruction to move middleware into the monorepo first was overtaken; its full
148
+ text is retained in the archived record.
149
+
package/docs/READINESS.md CHANGED
@@ -82,7 +82,10 @@ parameter patterns still need explicit negative fixtures to exercise them.
82
82
 
83
83
  `ready: true` requires a nonempty active project, matching expected count (when
84
84
  supplied), zero failed checks and no uncovered active route/method combinations.
85
- It means this local gate passed, not that all branches, parameter values or assets
85
+ When `ready` is false, `notReadyReasons` lists each failed condition:
86
+ `no-active-routes`, `route-count-mismatch`, `failed-checks` and
87
+ `uncovered-route-methods` (see `uncovered` for the pairs). `unassertedCases` never
88
+ affects `ready`. It means this local gate passed, not that all branches, parameter values or assets
86
89
  have independent business assertions. Function routes intentionally serving only
87
90
  errors cannot satisfy normal-response coverage in this release. Time-dependent
88
91
  expiry is evaluated at audit start; avoid running a gate exactly at expiry.
package/docs/README.md CHANGED
@@ -1,10 +1,20 @@
1
1
  # URLCode documentation
2
2
 
3
- Start with [the framework](FRAMEWORK.md): the four packages, the ladder from
4
- redirects to a full application, the composition contract and the rules an AI
5
- agent must follow. [Project direction](PROJECT-DIRECTION.md) states the
6
- boundary and the license. Use documentation pinned to your runtime revision;
7
- `version: "1"` is the stable project-format contract for this release line.
3
+ Choose a starting point, then use the topic directory below when you need detail.
4
+ Use documentation from the same pinned revision as your runtime.
5
+
6
+ | I want to… | Start here |
7
+ |---|---|
8
+ | Understand what URLCode does | [Framework](FRAMEWORK.md) |
9
+ | Build my first project | [Installation](INSTALL.md), then [YAML guide](YAML-GUIDE.md) |
10
+ | Build a site with UI, accounts and admin | [Composing a site](COMPOSING-A-SITE.md) |
11
+ | Have an AI author a project | [AI authoring](AI-AUTHORING.md), [agent index](../llms.txt) |
12
+ | Deploy and operate a project | [Operations](OPERATIONS.md) |
13
+ | Contribute to URLCode | [Contributing](../CONTRIBUTING.md), [local development](LOCAL-DEVELOPMENT.md) |
14
+
15
+ The [specification](SPECIFICATION.md) owns implemented semantics; the
16
+ [generated field reference](YAML-REFERENCE.md) lists accepted fields.
17
+ [Project direction](PROJECT-DIRECTION.md) explains the product boundary.
8
18
 
9
19
  ## Author a project
10
20
 
@@ -13,11 +23,9 @@ boundary and the license. Use documentation pinned to your runtime revision;
13
23
  | Install the CLI | [Installation](INSTALL.md) |
14
24
  | Write YAML with examples | [YAML guide and recipes](YAML-GUIDE.md) |
15
25
  | Look up every accepted field | [Generated field reference](YAML-REFERENCE.md), [JSON Schema](../schemas/urlcode.schema.json) |
16
- | Let an AI build routes | [AI authoring guide](AI-AUTHORING.md), [llms.txt](../llms.txt) |
17
26
  | Load authoring/operations rules into an agent | [Authoring skill](../.claude/skills/urlcode-authoring/SKILL.md), [operations skill](../.claude/skills/urlcode-operations/SKILL.md), [how they are distributed](AI-AUTHORING.md#agent-skills) |
18
- | Run examples | [25-route cookbook](../examples/cookbook/README.md), [prerender recipe](../examples/prerender/README.md), [small starter](STARTERS.md) |
19
27
  | Understand exact behavior | [Specification](SPECIFICATION.md), [routing](ROUTING.md), [HTTP](HTTP.md) |
20
- | Run examples | [40-route cookbook](../examples/cookbook/README.md), [prerender recipe](../examples/prerender/README.md), [small starter](STARTERS.md) |
28
+ | Run examples | [Executable cookbook](../examples/cookbook/README.md), [prerender recipe](../examples/prerender/README.md), [small starter](STARTERS.md) |
21
29
  | Let an AI build routes | [The framework](FRAMEWORK.md), [AI authoring guide](AI-AUTHORING.md), [llms.txt](../llms.txt), [SDK and read-only MCP](TOOLING.md) |
22
30
  | Reuse code around routes | [Middleware](MIDDLEWARE.md), [middleware examples](MIDDLEWARE-EXAMPLES.md) |
23
31
  | Handle secrets and decide what to sandbox | [Function security](FUNCTION-SECURITY.md) |
@@ -43,7 +51,6 @@ boundary and the license. Use documentation pinned to your runtime revision;
43
51
  | Manage users, sessions, roles and audit | [urlcode-admin](../packages/admin#readme) |
44
52
  | Restyle every extension page and translate copy | [urlcode-ui](../packages/ui#readme), [ui contract](../packages/ui/CONTRACT.md) |
45
53
  | Write or install a versioned extension | [Extensions](EXTENSIONS.md), [example fixture](../examples/extensions/README.md) |
46
- | Follow implementation of the auth, admin and UI extensions | [Extension implementation sequence](archive/2026-09-19/EXTENSION-IMPLEMENTATION.md) |
47
54
  | Know which core version an extension package supports, and how it says so | [Core version alignment](VERSION-ALIGNMENT.md) |
48
55
  | Add host behavior in operator code | [Plugins](PLUGINS.md) |
49
56
  | Use the API from TypeScript | [TypeScript: shipped declarations, exports, build and fidelity](TYPESCRIPT.md) |
@@ -59,7 +66,6 @@ boundary and the license. Use documentation pinned to your runtime revision;
59
66
  | Review security findings and gaps | [Internal security audit](SECURITY-AUDIT.md) |
60
67
  | Assess release readiness | [Evidence and open gates](RELEASE-READINESS.md) |
61
68
  | See unfinished work | [Roadmap](../ROADMAP.md) |
62
- | Read why per-route Lambda compilation was declined | [Lambda compile spike (archived)](archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md) |
63
69
  | Verify a running deployment matches the project | [Deployment checks](DEPLOYMENT-CHECKS.md) |
64
70
  | Inspect target support | [Capabilities and normalized representation](CAPABILITIES.md) |
65
71
  | Deploy to Vercel, AWS Lambda or Cloudflare Workers | [Vercel](VERCEL.md), [AWS](AWS.md), [Cloudflare](CLOUDFLARE.md), [provider verification evidence](PROVIDER-VERIFICATION.md) |
@@ -0,0 +1,30 @@
1
+ # URLCode 0.4.2
2
+
3
+ Core, UI, auth and admin share this explicitly selected stable version. Independent package versioning remains enabled.
4
+
5
+ ```sh
6
+ npm install --save-exact @jimhoyd/urlcode@0.4.2 @jimhoyd/urlcode-ui@0.4.2 @jimhoyd/urlcode-auth@0.4.2 @jimhoyd/urlcode-admin@0.4.2
7
+ ```
8
+
9
+ ### admin-copy-project-override.md
10
+
11
+ Project translations of `adminUi.*` ids in `ui/copy/<locale>.json` now reach the admin console.
12
+
13
+ In a composed site the console copy source is built with `createAdminPresentation({ base: kit.presentation })`, and the admin ids were resolved only from the bundled English, so a project's `adminUi.*` entries were silently ignored. When a base presentation is given, an admin id the base resolves for the request's locale now wins; otherwise the bundled admin English still answers. Hosts that pass no `base` and hosts that supply their own presentation are unchanged.
14
+
15
+ ### ui-styles-once-per-verify.md
16
+
17
+ Compile Tailwind once per `npm run verify`.
18
+
19
+ No API change and no change to the generated stylesheet. `verify` used to run
20
+ `typecheck` then `build`, and each of those runs `styles`, so
21
+ `scripts/build-styles.mjs` compiled the same minified CSS twice per
22
+ verification. `verify` now runs `styles` once and then the compiler-only
23
+ `typecheck:tsc` and `build:tsc` scripts. `typecheck` and `build` are unchanged
24
+ from a caller's point of view: each still runs `styles` first, so either one
25
+ works on its own from a fresh checkout.
26
+
27
+ Nothing is cached and nothing is skipped because an output already exists; the
28
+ single run is unconditional, so a source change is still picked up.
29
+
30
+ Publish to the npm `latest` channel only after exact-commit CI and candidate verification. Existing tags and the `alpha` channel stay unchanged. Update the standalone starter after core registry installability is verified. This preparation is not evidence of publication or an independent security assessment.