@jimhoyd/urlcode 0.4.0-alpha.2 → 0.4.1

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 (147) hide show
  1. package/.claude/skills/urlcode-authoring/SKILL.md +17 -19
  2. package/.claude/skills/urlcode-operations/SKILL.md +9 -9
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CONTRIBUTING.md +20 -16
  5. package/README.md +59 -64
  6. package/ROADMAP.md +66 -376
  7. package/dist/BUILD-MANIFEST.json +26 -25
  8. package/dist/agents-guide.js +6 -6
  9. package/dist/authoring.js +15 -1
  10. package/dist/build-static.js +2 -0
  11. package/dist/capability-query.js +0 -1
  12. package/dist/catalog.js +0 -1
  13. package/dist/cli.js +25 -9
  14. package/dist/config.js +1 -1
  15. package/dist/explain-cli.js +4 -2
  16. package/dist/explain.js +8 -2
  17. package/dist/extensions.js +1 -1
  18. package/dist/http-response.js +1 -1
  19. package/dist/index.js +1 -0
  20. package/dist/init-with.js +36 -11
  21. package/dist/manifest.js +8 -2
  22. package/dist/mcp-authoring.js +4 -4
  23. package/dist/mcp.js +1 -1
  24. package/dist/policies/cache.js +2 -2
  25. package/dist/policies.js +3 -1
  26. package/dist/prerender.js +4 -0
  27. package/dist/project-dependencies.js +305 -0
  28. package/dist/readiness.js +3 -0
  29. package/dist/route-diff.js +12 -5
  30. package/dist/runtime.js +1 -1
  31. package/dist/trusted-functions.js +4 -5
  32. package/dist/types/authoring.d.ts +9 -1
  33. package/dist/types/capability-query.d.ts +0 -1
  34. package/dist/types/catalog.d.ts +0 -4
  35. package/dist/types/config.d.ts +1 -9
  36. package/dist/types/explain.d.ts +6 -1
  37. package/dist/types/extensions.d.ts +1 -1
  38. package/dist/types/http-response.d.ts +0 -1
  39. package/dist/types/index.d.ts +1 -0
  40. package/dist/types/init-with.d.ts +7 -13
  41. package/dist/types/manifest.d.ts +5 -2
  42. package/dist/types/project-dependencies.d.ts +78 -0
  43. package/dist/types/readiness.d.ts +2 -0
  44. package/dist/types/trusted-functions.d.ts +1 -4
  45. package/dist/types/types.d.ts +8 -1
  46. package/dist/types.js +8 -1
  47. package/dist/typescript-authoring.js +18 -7
  48. package/docs/AI-AUTHORING.md +15 -7
  49. package/docs/ASSETS.md +2 -1
  50. package/docs/AUTH-BACKUP.md +32 -0
  51. package/docs/AWS.md +9 -0
  52. package/docs/BEST-PRACTICES.md +17 -9
  53. package/docs/CAPABILITIES.md +1 -1
  54. package/docs/CI-FOLLOWUP-2026-09-19.md +97 -0
  55. package/docs/CI-RELEASE-AUDIT-2026-09-19.md +322 -0
  56. package/docs/CI.md +8 -3
  57. package/docs/CODEBASE-AUDIT-2026-09-20.md +284 -0
  58. package/docs/COMPOSING-A-SITE.md +278 -0
  59. package/docs/DEVELOPMENT-PIPELINE.md +270 -0
  60. package/docs/EXTENSIONS.md +51 -11
  61. package/docs/FRAMEWORK.md +67 -48
  62. package/docs/FUNCTION-SECURITY.md +44 -0
  63. package/docs/INSTALL.md +13 -8
  64. package/docs/LOCAL-DEVELOPMENT.md +3 -0
  65. package/docs/MIDDLEWARE.md +10 -4
  66. package/docs/OPEN-DECISIONS.md +224 -212
  67. package/docs/OPERATIONAL-PROOF.md +3 -3
  68. package/docs/OPERATIONS.md +3 -3
  69. package/docs/POLICIES.md +13 -5
  70. package/docs/PRERENDER.md +23 -11
  71. package/docs/PROJECT-DIRECTION.md +3 -3
  72. package/docs/READINESS.md +10 -5
  73. package/docs/README.md +20 -44
  74. package/docs/RELEASE-0.4.0-alpha.3.md +50 -0
  75. package/docs/RELEASE-0.4.1.md +73 -0
  76. package/docs/RELEASE-READINESS.md +6 -6
  77. package/docs/RELEASE-SECURITY.md +96 -181
  78. package/docs/RESILIENCE.md +3 -3
  79. package/docs/ROUTING.md +3 -4
  80. package/docs/SECURITY-AUDIT.md +2 -2
  81. package/docs/SPECIFICATION.md +12 -6
  82. package/docs/SPIKE-AI-FRAMEWORK-BENCHMARK.md +6 -5
  83. package/docs/SPIKE-BUSINESS-SUITE.md +14 -6
  84. package/docs/SPIKE-CORE-LAYERING.md +49 -18
  85. package/docs/SPIKE-DEFAULT-TRUST-MODEL.md +7 -5
  86. package/docs/STARTERS.md +17 -5
  87. package/docs/STATIC.md +14 -3
  88. package/docs/TOOLING.md +10 -7
  89. package/docs/TYPESCRIPT-AUTHORING.md +6 -1
  90. package/docs/VERCEL.md +10 -2
  91. package/docs/VERSION-ALIGNMENT.md +76 -201
  92. package/docs/archive/2026-09-19/EXTENSION-IMPLEMENTATION.md +68 -0
  93. package/docs/archive/2026-09-19/MANAGEMENT-SECURITY.md +102 -0
  94. package/docs/{NEXT-PHASE-PLAN.md → archive/2026-09-19/NEXT-PHASE-PLAN.md} +15 -5
  95. package/docs/{NEXT-STEPS.md → archive/2026-09-19/NEXT-STEPS.md} +15 -3
  96. package/docs/archive/2026-09-19/OPEN-DECISIONS.md +277 -0
  97. package/docs/archive/2026-09-19/RELEASE-SECURITY.md +186 -0
  98. package/docs/archive/2026-09-19/ROADMAP.md +387 -0
  99. package/docs/{SPIKE-EXTENSION-MODEL.md → archive/2026-09-19/SPIKE-EXTENSION-MODEL.md} +11 -0
  100. package/docs/{SPIKE-EXTENSIONS.md → archive/2026-09-19/SPIKE-EXTENSIONS.md} +25 -14
  101. package/docs/archive/2026-09-19/SPIKE-LAMBDA-COMPILE.md +365 -0
  102. package/docs/archive/2026-09-19/SPIKE-MONOREPO.md +778 -0
  103. package/docs/{USABILITY-REVIEW.md → archive/2026-09-19/USABILITY-REVIEW.md} +12 -2
  104. package/docs/archive/README.md +28 -0
  105. package/docs/policies/agents.md +1 -1
  106. package/docs/policies/compression.md +3 -2
  107. package/docs/policies/security.md +3 -2
  108. package/docs/yaml/functions.md +10 -2
  109. package/docs/yaml/middleware.md +5 -3
  110. package/examples/assets/example.yaml +1 -1
  111. package/examples/cookbook/middleware/envelope.mjs +4 -2
  112. package/examples/cookbook/route-index.json +1 -1
  113. package/examples/cookbook/routes/middleware.yaml +1 -1
  114. package/examples/prerender/README.md +14 -6
  115. package/examples/prerender/functions/page.mjs +4 -2
  116. package/examples/prerender/middleware/template.mjs +1 -1
  117. package/examples/prerender/prerender.mjs +1 -1
  118. package/examples/prerender/urlcode.yaml +8 -4
  119. package/llms-full.txt +503 -88
  120. package/llms.txt +6 -4
  121. package/package.json +27 -4
  122. package/packaging/claude-plugin/.claude-plugin/plugin.json +2 -2
  123. package/packaging/claude-plugin/skills/urlcode-authoring/SKILL.md +17 -19
  124. package/packaging/claude-plugin/skills/urlcode-operations/SKILL.md +9 -9
  125. package/recipes/authenticated-json-api/README.md +4 -3
  126. package/recipes/authenticated-json-api/functions/profile.mjs +2 -1
  127. package/recipes/authenticated-json-api/recipe.yaml +1 -1
  128. package/recipes/contact-form/functions/contact.mjs +2 -1
  129. package/recipes/contact-form/recipe.yaml +2 -2
  130. package/recipes/cors-api/README.md +2 -2
  131. package/recipes/cors-api/recipe.yaml +1 -1
  132. package/recipes/health-page/README.md +1 -1
  133. package/recipes/json-api/README.md +1 -1
  134. package/recipes/json-api/recipe.yaml +3 -3
  135. package/recipes/middleware/README.md +8 -4
  136. package/recipes/middleware/middleware/envelope.mjs +4 -2
  137. package/recipes/protected-download/README.md +1 -1
  138. package/recipes/protected-download/recipe.yaml +1 -1
  139. package/recipes/static-plus-api/README.md +2 -2
  140. package/recipes/static-plus-api/public/index.html +1 -1
  141. package/recipes/static-plus-api/recipe.yaml +1 -1
  142. package/recipes/static-plus-api/urlcode.yaml +1 -1
  143. package/recipes/typescript/recipe.yaml +4 -4
  144. package/skills/urlcode/SKILL.md +6 -6
  145. package/starters/default/AGENTS.md +6 -6
  146. package/docs/SPIKE-LAMBDA-COMPILE.md +0 -201
  147. package/docs/SPIKE-MONOREPO.md +0 -322
@@ -1,201 +0,0 @@
1
- # Spike: compiling `function` routes into their own Lambdas
2
-
3
- Status: proposal, nothing implemented. No code in this repository does any of
4
- this, and nothing here is committed scope.
5
-
6
- AWS already deploys today. `createLambdaHandler` (`src/aws.ts`) runs a project
7
- as **one** Lambda behind a Function URL or an API Gateway HTTP API, reading the
8
- same `urlcode.yaml` that runs locally — see [AWS](AWS.md). What it cannot serve
9
- is `function`, `middleware` and `link`, which
10
- `activateNativeOnly` (`src/adapters.ts`) refuses for the whole deployment at
11
- activation rather than letting individual routes fail per request.
12
-
13
- This spike asks one question: **is the refusal of `function` a fact about
14
- Lambda, or a fact about the adapter?** It argues the second, sketches the
15
- lowering that follows, and is deliberate about what that lowering costs.
16
-
17
- ## 1. Where the refusal actually comes from
18
-
19
- `src/capabilities.ts` gives the reason:
20
-
21
- ```
22
- capability === 'function' ? 'isolated functions need worker threads and the WASM engine'
23
- ```
24
-
25
- That is true of the runtime's *own* mechanism. Isolation for guest code is
26
- QuickJS inside WebAssembly, driven from worker threads, with the boundaries
27
- [function security](FUNCTION-SECURITY.md) lists: no `process`, no filesystem,
28
- no sockets, no `fetch`, a fresh guest heap per invocation, and bindings denied
29
- unless an operator granted them by exact name.
30
-
31
- A single Lambda cannot host that engine cheaply, because every cold start pays
32
- worker startup and WASM instantiation before the first request. So the adapter
33
- refuses — correctly, for the shape it is.
34
-
35
- But nothing in that sentence is about Lambda. It is about *one process serving
36
- every route*. Change the deployment unit and the sentence stops applying.
37
-
38
- ## 2. The lowering
39
-
40
- Cloudflare already establishes the pattern: where a platform forbids what the
41
- adapter needs, URLCode **compiles ahead of time** instead of adapting at
42
- runtime. `urlcode build --target cloudflare` (`src/build-cloudflare.ts`) emits
43
- an artifact the Worker reads, and refuses at build time anything it cannot
44
- serve, with the route named — see [Cloudflare](CLOUDFLARE.md).
45
-
46
- The same move for AWS: a build step emits **one Lambda per `function` route**,
47
- plus the existing native-handler Lambda for everything else.
48
-
49
- ```
50
- urlcode build --target aws --project . --out dist
51
-
52
- dist/
53
- routes/ the native handler Lambda (redirect, respond, page, static, download)
54
- fn/<route-id>/ one directory per function route
55
- template.yaml the generated stack
56
- ```
57
-
58
- The guest source becomes the Lambda's handler. There is no QuickJS in the
59
- request path, because the request never crosses a guest boundary inside a
60
- process — the process *is* the boundary.
61
-
62
- This is not a smaller change than it looks. Three things follow from it.
63
-
64
- ## 3. What changes, stated plainly
65
-
66
- ### 3.1 The isolation guarantee is replaced, not preserved
67
-
68
- This is the claim most likely to be made too early, so it goes first.
69
-
70
- QuickJS-WASM and a Lambda are both real isolation. They are **not the same
71
- isolation**, and neither strictly contains the other:
72
-
73
- | | QuickJS-WASM | per-route Lambda |
74
- | --- | --- | --- |
75
- | Network | unavailable unless a binding is granted | available by default; must be removed |
76
- | Filesystem | unavailable | a writable `/tmp`, and the deployment package |
77
- | Environment | not exposed to the guest | ambient unless scrubbed |
78
- | Blast radius of an escape | the guest heap | the function's IAM role |
79
- | Per-invocation state | fresh heap, guaranteed | a warm container may be reused |
80
-
81
- The two rows that matter most are the last two. A guest that escapes QuickJS
82
- reaches a heap. A guest that misbehaves in a Lambda reaches **whatever that
83
- Lambda's execution role can reach** — so the compiler would have to emit a role
84
- per route that grants exactly the route's declared bindings and nothing else,
85
- and that emitted role becomes a security-critical generated artifact.
86
- Warm-container reuse is the other: the runtime currently *guarantees* fresh
87
- state per invocation, and Lambda does not.
88
-
89
- The honest framing, and the one the docs would have to carry: per-route Lambdas
90
- are **a substitute for the sandbox, not the sandbox**. Anything that says
91
- "functions now work on AWS" without saying which guarantee changed is a claim
92
- this project should not make.
93
-
94
- ### 3.2 `middleware` is the hard part, not `function`
95
-
96
- `function` lowers cleanly because it is a leaf. `middleware` is a per-request
97
- chain, and there are only two ways to lower it, both with a real cost:
98
-
99
- - **Inline** the chain into each function Lambda at build time. Cheap at
100
- runtime; duplicates the middleware into every function's package, and a
101
- middleware change rebuilds every function.
102
- - **Orchestrate** — a hop per middleware. Composable; adds a Lambda invocation
103
- of latency and cost to every request, on the hot path.
104
-
105
- Neither is obviously right, which is exactly why this spike scopes middleware
106
- out rather than picking one under time pressure.
107
-
108
- ### 3.3 `link` does not fall out of this at all
109
-
110
- Stored live links need a durable writable store that instances share. That is
111
- the same refusal before and after this change. DynamoDB is the natural lowering,
112
- but it is a store implementation with its own export and restore discipline, not
113
- something a compile step produces. (Note: the native `link` handler this
114
- section describes was later removed from core; see
115
- `docs/SPIKE-CORE-LAYERING.md`.)
116
-
117
- ## 4. Emitting infrastructure is a new kind of output
118
-
119
- `examples/aws/template.yaml` is hand-written today. Generating a stack means
120
- this project starts owning a surface it has never owned:
121
-
122
- - The generated template is only as correct as the provider's current
123
- behaviour, which changes without asking.
124
- - A generated IAM role is a security artifact (3.1), so "the template is
125
- internal, don't rely on it" is a weaker disclaimer here than it was for the
126
- Cloudflare artifact.
127
- - The gap simply *moves* unless the emitted stack is checked against the
128
- project. `urlcode verify-deployment --target <url>` already probes a running
129
- deployment; it would need to cover the multi-Lambda shape, or the build gains
130
- a new unverified claim while retiring an honest refusal.
131
-
132
- That last point is the one worth holding onto. The project's current position on
133
- AWS is **honest**: the capability catalog reports `deployment: 'unverified'` for
134
- every target that is not self-hosted, and [AWS](AWS.md) tells a reader to treat
135
- the limits as unverified until they deploy the example themselves. A compiler
136
- that emits infrastructure nobody has deployed would be a larger unverified claim
137
- wearing the clothes of a capability.
138
-
139
- ## 5. The capability model already has the right shape
140
-
141
- `src/capabilities.ts` (from the target-capability centralization) is where this
142
- lands with no new concept:
143
-
144
- ```
145
- function / aws: refused → compiled
146
- ```
147
-
148
- `compiled` already exists as a `CapabilitySupport` value and already means what
149
- is needed here — Cloudflare uses it. `deployment` stays `'unverified'` until
150
- something is actually deployed. The catalog would tell the truth about the new
151
- lowering without anything else in the model changing, and `urlcode capabilities
152
- --target aws` would report it.
153
-
154
- ## 6. Proposed scope for a first spike
155
-
156
- Narrow, so that the isolation story stays clean and the win is real:
157
-
158
- **In:** `function` routes, Function URL only, one Lambda per function route,
159
- a generated role per route carrying exactly that route's granted bindings,
160
- and the existing native-handler Lambda unchanged for everything else.
161
-
162
- **Out:** `middleware` (3.2), `link` (3.3), API Gateway, VPC, custom domains,
163
- warm-start tuning, and any claim about cost.
164
-
165
- **Done looks like:** a project in `examples/` that builds, a generated template
166
- a reader can inspect, `urlcode capabilities --target aws` reporting `function`
167
- as `compiled`, and a written comparison of the two isolation models that a
168
- reviewer can disagree with.
169
-
170
- **Not done by that:** a deployment. Everything above can pass without anyone
171
- having run it on AWS, and the spike should say so rather than imply otherwise.
172
-
173
- ## 7. Prior art worth reading before building this
174
-
175
- - **Cloudflare target in this repository** — the closest precedent, and the one
176
- that establishes compile-not-adapt as a thing URLCode already does.
177
- - **SST, Serverless Framework, AWS CDK** — all generate per-function
178
- infrastructure from a declaration. The interesting question is not how they
179
- emit it but how they keep the emitted stack honest as the provider moves.
180
- - **Deno Deploy and Vercel functions** — isolate-per-request models, the closest
181
- commercial thing to the guarantee QuickJS-WASM gives today.
182
-
183
- ## 8. Open questions
184
-
185
- - Is warm-container reuse acceptable at all, given the runtime currently
186
- *guarantees* fresh per-invocation state? If not, this lowering is wrong for
187
- any route that relies on that guarantee, and there is no build-time way to
188
- tell which ones do.
189
- - Does the generated IAM role belong in URLCode's output, or should the build
190
- emit a *description* of the permissions each route needs and leave the role to
191
- the operator — closer to how [function security](FUNCTION-SECURITY.md) already
192
- keeps grants operator-controlled and outside the checkout?
193
- - Does a per-route Lambda change what `policies` can promise? `throttle` on AWS
194
- is already `conditional` — "counters are per instance" — and more instances
195
- make that weaker, not stronger.
196
- - Is one Lambda per route the right granularity, or one per *project* with a
197
- route parameter, which keeps deployment small but reintroduces a shared
198
- process?
199
- - What happens to the 6 MB Lambda response limit ([AWS](AWS.md)) for a function
200
- route that returns a large body — refuse at build time, as the Cloudflare
201
- target refuses what it cannot serve?
@@ -1,322 +0,0 @@
1
- # Spike: consolidating core, auth, admin, ui (and the two pending extractions) into one repo
2
-
3
- Status: proposal, nothing implemented, no repo touched. Drafted at the requester's
4
- explicit direction to produce a plan document only — see "What this is not"
5
- below. Treat this the same way as the other `SPIKE-*.md` documents in this
6
- directory: a recorded decision trail for the maintainer to accept, amend or
7
- reject, not committed scope.
8
-
9
- ## What this is not
10
-
11
- This is not a recommendation to touch any of `urlcode`, `urlcode-auth`,
12
- `urlcode-admin` or `urlcode-ui` tonight. No git history has been merged, no
13
- package has been moved, no CI has been reconfigured. Everything below is a
14
- sequenced plan to review, not a changelog of what happened.
15
-
16
- ## The problem this is answering
17
-
18
- Four repos (`urlcode`, `urlcode-auth`, `urlcode-admin`, `urlcode-ui`) already
19
- coordinate tightly — `auth`/`admin`/`ui` each pin an exact core revision in
20
- their own `peers.json`, and `docs/FRAMEWORK.md` describes them as one
21
- composed product, not four independent ones. Concretely observed cost of that
22
- coordination happening across four repos, from an evening spent reading all
23
- four:
24
-
25
- - **Observed and since fixed, which is the point rather than a counterpoint.**
26
- When core landed trusted-by-default execution (`b3bde4e`), `urlcode-auth`
27
- and `urlcode-admin` were both still pinning core at `50790d3a`
28
- (`0.4.0-alpha.1`), predating it, and `urlcode-auth/SECURITY.md` still
29
- carried a sentence ("sandboxed guest code") that assumed the old model.
30
- Both have since been corrected — both repos now pin `d5e86017`, and that
31
- sentence is gone. Nothing was ever broken in production by either.
32
- The cost this plan is describing is not "drift goes unnoticed forever"; it
33
- is that catching and fixing it took a manual pass across three separate
34
- repositories, with nothing structural to catch it automatically — no
35
- mechanism flags a downstream repo's prose or pin as stale when an upstream
36
- contract changes underneath it. That pass has to be repeated by hand on
37
- every future contract change, for every downstream repo, indefinitely.
38
- Consolidation removes the class of work, not just this instance of it.
39
- - Two more repos, planned in `docs/SPIKE-CORE-LAYERING.md` and originally
40
- drafted here as "not yet created," turned out to already exist by the time
41
- this doc was reviewed: `urlcode-dynamic-link` (7 commits, Phase 2 already
42
- implemented, `v0.1.0-alpha.1` released) and `urlcode-middleware` (5 commits,
43
- implemented, `v0.1.0-alpha.1` released), each with its own real commit
44
- history, release workflow and open issues. That raises the
45
- actively-coordinated repo count from four to six today, not hypothetically
46
- — before this plan even accounts for `urlcode-template`, `urlcode-short`,
47
- `urlcode-docs`, `urlcode-cloud` and `homebrew-urlcode`. It also means
48
- "create them directly in the monorepo" (this doc's original framing) is no
49
- longer available for these two — they now need the same history-preserving
50
- migration as `auth`/`admin`/`ui`, covered in "Migration mechanics" below.
51
-
52
- None of this is a defect in any one repo. It's the accumulating tax of
53
- coordinating tightly-coupled, independently-versioned packages across
54
- separate git histories, issue trackers and CI pipelines by hand.
55
-
56
- ## Scope: what moves, what doesn't
57
-
58
- Decided (see conversation this spike is drafted from):
59
-
60
- **In scope — six existing repos, all with real history, folded into one
61
- repo as workspace packages:**
62
-
63
- | Repo today | Becomes |
64
- |---|---|
65
- | `urlcode` (core) | `packages/core` (or repo root stays core-shaped, TBD in "Layout options" below) |
66
- | `urlcode-auth` | `packages/auth` |
67
- | `urlcode-admin` | `packages/admin` |
68
- | `urlcode-ui` | `packages/ui` |
69
- | `urlcode-dynamic-link` (real repo, `v0.1.0-alpha.1` released) | `packages/dynamic-link` |
70
- | `urlcode-middleware` (real repo, `v0.1.0-alpha.1` released) | `packages/middleware` |
71
-
72
- **Explicitly out of scope, each for a distinct, real reason — not just "left
73
- for later":**
74
-
75
- - **`homebrew-urlcode`** — cannot move. Homebrew tap conventions require a
76
- repo literally named `homebrew-<name>`; this is an external platform
77
- constraint, not a project choice.
78
- - **`urlcode-docs`** — `AGENTS.md` is explicit that public documentation is
79
- "authored there directly," deliberately separate from code, "no longer
80
- generated from this repository." Folding it in would reverse a stated,
81
- recent decision, not follow one.
82
- - **`urlcode-cloud`** — a separately-lifecycled hosted product (private
83
- repo); its release cadence and access model have no reason to match a
84
- library monorepo's.
85
- - **`urlcode-template` / `urlcode-short`** — these are example/starter
86
- projects, not library packages. Mixing "things you `npm install`" with
87
- "things you `git clone` as a starting point" in one workspace is a
88
- different kind of repo than what this spike is solving for.
89
-
90
- ## Why six, and not four
91
-
92
- `link` and `middleware` were extracted *out* of core specifically so core
93
- stays "the smallest thing that is still a complete product on its own"
94
- (`docs/SPIKE-CORE-LAYERING.md`). Both are now real, shipped repos: they
95
- already paid the coordination cost this spike is trying to remove —
96
- `urlcode-dynamic-link`'s and `urlcode-middleware`'s own `peers.json`-style
97
- pins against core, their own CI, their own docs that can drift the same way
98
- `urlcode-auth/SECURITY.md` already did. Folding them into this consolidation
99
- alongside `auth`/`admin`/`ui` stops that from compounding further, rather
100
- than leaving two more repos outside the fix.
101
-
102
- ## Layout: decided — option A
103
-
104
- **A. Root repo is core, extensions live under `packages/`.**
105
- ```
106
- urlcode/
107
- src/ # core, unchanged in place
108
- packages/
109
- auth/
110
- admin/
111
- ui/
112
- dynamic-link/
113
- middleware/
114
- ```
115
- Lowest-friction for core's own history (nothing moves), but makes "core" and
116
- "the monorepo" the same name, which may read as core absorbing the
117
- extensions rather than the extensions and core coexisting as peers — worth a
118
- naming discussion given `AGENTS.md`'s "Core never imports them" independence
119
- framing.
120
-
121
- **B. Everything moves under `packages/`, including core — considered, not
122
- chosen.** Would have been symmetric and avoided the naming overlap noted
123
- above, at real cost: core's own history would need to move too, and every
124
- external reference to `urlcode`'s current repo path (`docs/`, READMEs
125
- elsewhere, the `@jimhoyd/urlcode` package's repository field, CI badges,
126
- this evening's own `peer-camera`/`peer-eyes` citations) would need updating.
127
- Decided against for exactly that reason.
128
-
129
- **Decided: (A).** Core's repo and history stay exactly where they are; the
130
- six packages move to it (five extensions plus core itself now living in the
131
- same repo as a `packages/*` sibling). The one open item this still leaves,
132
- worth a short naming discussion rather than blocking anything: "core" and
133
- "the consolidated repo" now share a name, which could read as core absorbing
134
- the extensions rather than the two coexisting as independent packages
135
- (`AGENTS.md`'s "Core never imports them" framing still holds in code either
136
- way — this is a naming-perception question, not a contract question).
137
-
138
- ## Migration mechanics, per repo
139
-
140
- For each of `urlcode-auth`, `urlcode-admin`, `urlcode-ui`,
141
- `urlcode-dynamic-link` and `urlcode-middleware` — all six now real repos
142
- with real history:
143
-
144
- 0. **Drain open pull requests first — a hard precondition, not a courtesy.**
145
- Before a repo is migrated, it must have zero open PRs (and no unmerged
146
- release branch). A PR open against the source repo at the moment its code
147
- moves is stranded: its branch targets a `main` that no longer receives
148
- code, its diff is written against paths (`src/…`) that no longer exist at
149
- that location, and re-creating it against the consolidated repo means
150
- rebasing onto a different repository and a new path prefix
151
- (`packages/<name>/src/…`) by hand. GitHub cannot retarget a PR across
152
- repositories. So for each repo, in order: stop merging new work, merge or
153
- close what is open, confirm `gh pr list`/the API reports none, then
154
- migrate. Any PR that cannot be merged in time should be closed with its
155
- branch preserved and re-opened against the consolidated repo afterwards —
156
- a deliberate choice recorded on the PR, not an accident discovered later.
157
- This is also the real reason to pick a quiet window for the migration
158
- rather than a busy one: the cost of this step scales with how much is
159
- in flight.
160
- 1. **Preserve history with `git subtree add` or `git filter-repo` +
161
- merge**, not a fresh copy — so `git log`/`git blame` on
162
- `packages/auth/src/auth.ts` still resolves to the real authorship history
163
- from `urlcode-auth`, and so a future "actually, let's give this its own
164
- repo back" is a clean `git filter-repo` extraction, not archaeology.
165
- `git subtree` is the lower-risk default (reversible, no force-push
166
- required on the source repos); `git filter-repo` gives cleaner resulting
167
- history at the cost of being a one-way rewrite of the joining repo's
168
- local copy (the original `urlcode-auth` GitHub repo is untouched either
169
- way — this only rewrites what gets pulled in).
170
- 2. **npm workspace restructuring**: `package.json` at the monorepo root gets
171
- `"workspaces": ["packages/*"]` (the same shape `peer-camera` already
172
- uses); each `packages/<name>/package.json` keeps its own name/version,
173
- independently publishable — this is what preserves "independently
174
- versioned packages" as a property, not something this migration gives up.
175
- **Decided: [Changesets](https://github.com/changesets/changesets) for the
176
- release flow, not Nx or Turborepo.** A changeset is a small, bounded,
177
- git-diffable markdown file (package name + semver bump + description) —
178
- cheap and low-risk for an agent or a human to generate correctly, easy
179
- for CI to verify mechanically ("does every touched package have one"),
180
- and it's the deliberate checkpoint that stops local workspace-linked
181
- development (testing against a sibling package's unreleased state, which
182
- is now the default once auth/admin/ui/dynamic-link/middleware sit next to
183
- core) from silently becoming a real release. Nx/Turborepo were considered
184
- and set aside: both add a much larger, more inference-heavy configuration
185
- surface (task graphs, remote caching semantics) that's a bigger, more
186
- opaque thing to get wrong than this repo's six packages currently need —
187
- plain `npm test -w packages/auth`-style workspace scoping already covers
188
- what this size of repo actually requires. Revisit only if the package
189
- count grows enough that rebuild/retest time becomes a real problem.
190
- 3. **`peers.json` becomes unnecessary for the six that moved** — a
191
- workspace package can depend on a sibling workspace package directly
192
- (`"@jimhoyd/urlcode": "workspace:*"` or npm's equivalent), which is
193
- inherently always in sync, no separate pin file, no drift possible by
194
- construction. `peers.json`-the-mechanism might still matter if any
195
- *external* consumer needs a reviewed-revision pin story — worth deciding
196
- explicitly rather than silently dropping the safeguard.
197
- 4. **CI consolidation**: one `verify.yml` (or similar) with
198
- path-filtered jobs per package, replacing four separate workflow files.
199
- `CODEOWNERS` can still express per-package ownership within one repo
200
- (path-scoped rules), so "who reviews auth changes" doesn't have to
201
- become "everyone reviews everything."
202
- 5. **Docs cross-references**: every `[EXTENSIONS.md](../urlcode/docs/...)`-
203
- style cross-repo link in `auth`/`admin`/`ui`'s current docs becomes a
204
- same-repo relative link once consolidated — this is a real cleanup
205
- opportunity, not just migration overhead, since it directly targets the
206
- "docs silently drifted apart" problem this spike opened with.
207
- 6. **Re-register npm Trusted Publishing per package.** All six repos'
208
- release workflows publish via OIDC trusted publishing, no long-lived npm
209
- token (`docs/SPIKE-CORE-LAYERING.md`'s governance section, confirmed by
210
- `urlcode-dynamic-link`'s and `urlcode-middleware`'s own "Add
211
- trusted-publishing release workflow" commits). That trust is registered
212
- on npmjs.com per package, pinned to an exact GitHub repo + workflow
213
- filename (+ optional environment) — it does not follow the code when the
214
- repo path changes. Each of `@jimhoyd/urlcode-auth`, `-admin`, `-ui`,
215
- `-dynamic-link`, `-middleware` needs its npmjs.com trusted-publisher entry
216
- updated to the new repo and new workflow path *before* that package's
217
- first release from the consolidated location, or the publish step fails
218
- closed (correctly — not a security gap, just an ordering dependency this
219
- plan needs to carry explicitly rather than discover at release time).
220
- 7. **Issue migration — decided: recreate open issues in the consolidated
221
- repo, not leave-and-link.** GitHub doesn't move issues across repos
222
- natively, so this means bulk-recreating each open issue at the new
223
- location with a back-link to the original (closed with a pointer) rather
224
- than leaving it where it is. Concrete scope as of this doc: `auth`,
225
- `admin` and `ui`'s own open-issue counts weren't re-audited here, but
226
- `urlcode-dynamic-link` and `urlcode-middleware` were, since they're the
227
- two repos whose "does this even apply" status changed mid-conversation:
228
- - `urlcode-dynamic-link`: 0 open issues — nothing to migrate.
229
- - `urlcode-middleware`: 2 open issues to recreate —
230
- [`#1`](https://github.com/jimhoyd-com/urlcode-middleware/issues/1)
231
- ("`sandbox: true` is not supported — needs its own QuickJS/WASM worker
232
- pool") and
233
- [`#3`](https://github.com/jimhoyd-com/urlcode-middleware/issues/3)
234
- ("Remove vendored core tarball once `@jimhoyd/urlcode` 0.4.0-alpha.2+ is
235
- published to npm"). Both should move to the consolidated repo's tracker
236
- when the merge actually happens, each closed in its original location
237
- with a link to the new issue.
238
-
239
- ## What this preserves, unchanged
240
-
241
- - **The trust/extension model itself.** `packages/auth` published from the
242
- monorepo is exactly as separate a package, with exactly the same
243
- `RuntimeExtension` contract, revision-pinning and operator-registration
244
- requirements, as `urlcode-auth` published from its own repo today. This
245
- spike changes where the source lives, not what the extension mechanism
246
- guarantees.
247
- - **Independent versioning and release cadence per package** — a monorepo
248
- with workspaces is not "one version number for everything."
249
-
250
- ## What this gives up, honestly
251
-
252
- - **Per-repo maturity gating.** `docs/SPIKE-CORE-LAYERING.md` records that
253
- `auth`/`admin`/`ui` used a "`private: true` until reviewed" pattern before
254
- their first public release, and that the two new repos are deliberately
255
- *not* following that pattern ("published public from the start"). A
256
- monorepo can't easily make one folder private and another public — the
257
- repo-level visibility setting is all-or-nothing on GitHub. Once
258
- consolidated, "private until reviewed" stops being available as a pattern
259
- for whatever the next extension after `middleware`/`dynamic-link` turns
260
- out to be, unless it's built in yet another separate private repo first
261
- and merged in later — which reintroduces a version of the coordination
262
- cost this spike is trying to remove, just for pre-release work instead of
263
- ongoing maintenance.
264
- - **"Fork just one piece" stops being a plain `git clone` — but scoped to a
265
- narrow audience, not every auth user.** `SPIKE-AUTH.md` names forkability
266
- as a deliberate design goal specifically for `auth`. It's important not to
267
- overstate who this actually affects: a developer customizing auth's look
268
- or copy (theme, relabeling, `extra.css`, a shadowed template) works
269
- entirely inside *their own* project repo via the `ui` extension's layering
270
- system (`ui/copy`, `ui/extra.css`, `ui/templates`) — they never clone or
271
- fork `urlcode-auth` at all, install it from npm like any dependency, and
272
- this migration changes nothing for them. The friction increase applies
273
- only to the much narrower case of someone changing auth's actual *logic*
274
- (a new sign-in method, different session semantics) — something the
275
- layering system can't express because it's behavior, not presentation.
276
- For that persona, forking just the auth package post-consolidation means a
277
- `git filter-repo`-style history extraction instead of `git clone
278
- jimhoyd-com/urlcode-auth` — solvable, but a real step up in friction, for
279
- a small population, not the common path.
280
- - **Blast radius of a bad CI run.** One consolidated CI means a
281
- misconfigured job can, in principle, block merges across all six
282
- packages at once, where today a broken `urlcode-ui` pipeline can't stop an
283
- unrelated `urlcode-auth` merge. Path-filtered jobs mitigate this but don't
284
- eliminate it the way full repo separation does.
285
-
286
- ## Sequencing, if this is accepted
287
-
288
- 1. Decide layout (A vs. B above) and confirm the out-of-scope list.
289
- 2. **Check open pull requests across all six repos before starting, and again
290
- per repo immediately before its own migration** (mechanics #0). A repo with
291
- anything open is not ready to move. Doing this as a survey first also sizes
292
- the whole migration honestly: the number of in-flight PRs is the real
293
- scheduling constraint, not the git mechanics.
294
- 3. Migrate `urlcode-ui` first (fewest inbound dependents — `auth`/`admin`
295
- both depend on it, nothing depends on them), proving the subtree +
296
- workspace mechanics on the lowest-risk package. Re-register its npm
297
- trusted publisher (mechanics #6) before cutting its first release from
298
- the new location — treat this as part of "done," not a follow-up.
299
- 4. Migrate `urlcode-auth`, then `urlcode-admin` — same re-registration step
300
- each time.
301
- 5. Migrate `urlcode-dynamic-link`, then `urlcode-middleware` — same
302
- subtree/filter-repo mechanics and trusted-publisher re-registration as
303
- the other three, now that both are real repos with real history rather
304
- than something created fresh in place. Recreate their open issues (see
305
- "Migration mechanics" #7 above: 0 from `dynamic-link`, `#1` and `#3` from
306
- `middleware`) in the consolidated tracker as part of each repo's
307
- migration step, not as a separate pass.
308
- 6. Retire (archive, don't delete — GitHub redirects an archived repo's clone
309
- URL) all six now-empty source repos, with their READMEs pointing at the
310
- new location.
311
-
312
- ## Open questions for the maintainer, not answered here
313
-
314
- - Does `peers.json`'s reviewed-pin discipline need an equivalent for any
315
- external (non-workspace) consumer, or does workspace-linking fully replace
316
- its purpose?
317
- - `git subtree` vs. `git filter-repo` for history preservation — a real
318
- tradeoff between migration safety and final history cleanliness, worth a
319
- deliberate call rather than defaulting.
320
- - The naming-perception question from "Layout: decided — option A" above
321
- (core's repo and the consolidated repo sharing a name) — worth a short
322
- discussion, not blocking.