@voltro/cli 0.16.0 → 0.18.0

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 (139) hide show
  1. package/CHANGELOG.md +297 -0
  2. package/bin/voltro.mjs +6 -1
  3. package/dist/{apiBuild-BJtiqoOu.js → apiBuild-CPDTJHkH.js} +2 -2
  4. package/dist/apiBuild-D1UBJ4TM.js +2 -0
  5. package/dist/bin.js +3 -3
  6. package/dist/{commands-8A5RU3MW.js → commands-laJDMj2m.js} +2679 -2444
  7. package/dist/dbCommand-C3R5LrBZ.js +2 -0
  8. package/dist/{dbCommand-OPMtO0sX.js → dbCommand-DJSxnjPt.js} +2 -2
  9. package/dist/{dev-DGjEvvkV.js → dev-BOFxC21E.js} +1076 -1013
  10. package/dist/{dev-HdG7JdSk.js → dev-DOK0w6ZW.js} +1 -1
  11. package/dist/{fileConventions-Cu0tM-Bl.js → fileConventions-CBNPZVOV.js} +1 -1
  12. package/dist/frameworkTableAssembly-DRJrAVr1.js +2 -0
  13. package/dist/{frameworkTableAssembly-BA2bAmYN.js → frameworkTableAssembly-sg41s14F.js} +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/{inspectMetrics-CYDSO3za.js → inspectMetrics-D1DmLeJs.js} +491 -478
  16. package/dist/{serveCommand-C3lq3mFd.js → serveCommand-CxcxHc9Y.js} +322 -322
  17. package/dist/serveEntry.js +2 -2
  18. package/dist/{start-BtgatJN3.js → start-Clvz4IJb.js} +379 -379
  19. package/dist/startEntry.js +2 -2
  20. package/package.json +17 -17
  21. package/templates/AGENTS.core.md +62 -9
  22. package/templates/AGENTS.md +63 -10
  23. package/templates/agent-docs/_index.md +1 -1
  24. package/templates/agent-docs/authentication.md +48 -0
  25. package/templates/agent-docs/cli.md +11 -7
  26. package/templates/agent-docs/database/migrations.md +3 -3
  27. package/templates/agent-docs/database/querying.md +6 -2
  28. package/templates/agent-docs/internationalization.md +3 -3
  29. package/templates/agent-docs/introduction.md +17 -3
  30. package/templates/agent-docs/reference.md +4 -4
  31. package/templates/agent-docs/routing.md +42 -40
  32. package/templates/agent-docs/schema-driven-ui.md +6 -3
  33. package/templates/agent-docs/templates/appshells.md +24 -24
  34. package/templates/agent-docs/whats-new.md +170 -65
  35. package/templates/apps/api-ai/package.json +7 -7
  36. package/templates/apps/api-auth/package.json +8 -8
  37. package/templates/apps/api-backend/package.json +7 -7
  38. package/templates/apps/api-backend-deactivation/package.json +7 -7
  39. package/templates/apps/api-backend-mail/package.json +8 -8
  40. package/templates/apps/api-backend-mariadb/package.json +9 -9
  41. package/templates/apps/api-backend-storage/package.json +8 -8
  42. package/templates/apps/api-data-advanced/package.json +8 -8
  43. package/templates/apps/api-durable/package.json +8 -8
  44. package/templates/apps/api-feature-flags/package.json +9 -9
  45. package/templates/apps/api-governance/package.json +8 -8
  46. package/templates/apps/api-kv/package.json +8 -8
  47. package/templates/apps/api-moderation/package.json +8 -8
  48. package/templates/apps/api-observability/package.json +8 -8
  49. package/templates/apps/api-ratelimit/package.json +8 -8
  50. package/templates/apps/api-rbac/package.json +8 -8
  51. package/templates/apps/api-rest/package.json +7 -7
  52. package/templates/apps/api-saas/package.json +11 -11
  53. package/templates/apps/api-search/package.json +8 -8
  54. package/templates/apps/api-versioning/package.json +8 -8
  55. package/templates/apps/api-webhooks/package.json +9 -9
  56. package/templates/apps/changelog/package.json +6 -6
  57. package/templates/apps/changelog/src/pages/[locale]/{[slug].page.tsx → [slug]/page.tsx} +4 -4
  58. package/templates/apps/changelog/src/pages/[locale]/mirrors.test.tsx +2 -2
  59. package/templates/apps/changelog/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  60. package/templates/apps/changelog/src/pages/{[slug].page.test.tsx → [slug]/page.test.tsx} +1 -1
  61. package/templates/apps/changelog/src/pages/{[slug].page.tsx → [slug]/page.tsx} +2 -2
  62. package/templates/apps/changelog/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  63. package/templates/apps/edge-functions/package.json +2 -2
  64. package/templates/apps/frontend-admin/package.json +8 -8
  65. package/templates/apps/frontend-admin/src/pages/(marketing)/{login.page.test.tsx → login/page.test.tsx} +2 -2
  66. package/templates/apps/frontend-admin/src/pages/(marketing)/{login.page.tsx → login/page.tsx} +2 -2
  67. package/templates/apps/frontend-admin/src/pages/(marketing)/{index.page.test.tsx → page.test.tsx} +1 -1
  68. package/templates/apps/frontend-admin/src/pages/admin/{[entity].page.tsx → [entity]/page.tsx} +1 -1
  69. package/templates/apps/frontend-admin/src/pages/admin/entity.test.tsx +1 -1
  70. package/templates/apps/frontend-admin/src/pages/admin/{index.page.test.tsx → page.test.tsx} +1 -1
  71. package/templates/apps/frontend-app/package.json +8 -8
  72. package/templates/apps/frontend-app/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  73. package/templates/apps/frontend-app/src/pages/{schema-ui.page.test.tsx → schema-ui/page.test.tsx} +2 -2
  74. package/templates/apps/frontend-app/src/pages/{schema-ui.page.tsx → schema-ui/page.tsx} +1 -1
  75. package/templates/apps/frontend-blank/package.json +7 -7
  76. package/templates/apps/frontend-blank/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  77. package/templates/apps/frontend-contact/package.json +7 -7
  78. package/templates/apps/frontend-contact/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  79. package/templates/apps/frontend-contact/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  80. package/templates/apps/frontend-dashboard/package.json +7 -7
  81. package/templates/apps/frontend-dashboard/src/pages/(marketing)/{login.page.test.tsx → login/page.test.tsx} +2 -2
  82. package/templates/apps/frontend-dashboard/src/pages/(marketing)/{login.page.tsx → login/page.tsx} +2 -2
  83. package/templates/apps/frontend-dashboard/src/pages/(marketing)/{index.page.test.tsx → page.test.tsx} +1 -1
  84. package/templates/apps/frontend-dashboard/src/pages/dashboard/{index.page.test.tsx → page.test.tsx} +1 -1
  85. package/templates/apps/frontend-dashboard/src/pages/dashboard/{settings.page.test.tsx → settings/page.test.tsx} +2 -2
  86. package/templates/apps/frontend-dashboard/src/pages/dashboard/{settings.page.tsx → settings/page.tsx} +2 -2
  87. package/templates/apps/frontend-docs/package.json +7 -7
  88. package/templates/apps/frontend-docs/src/pages/[locale]/docs/{[...slug].page.tsx → [...slug]/page.tsx} +4 -4
  89. package/templates/apps/frontend-docs/src/pages/[locale]/mirrors.test.tsx +2 -2
  90. package/templates/apps/frontend-docs/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  91. package/templates/apps/frontend-docs/src/pages/docs/{[...slug].page.test.tsx → [...slug]/page.test.tsx} +3 -3
  92. package/templates/apps/frontend-docs/src/pages/docs/{[...slug].page.tsx → [...slug]/page.tsx} +1 -1
  93. package/templates/apps/frontend-docs/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  94. package/templates/apps/frontend-i18n/package.json +6 -6
  95. package/templates/apps/frontend-i18n/src/pages/[locale]/{about.page.tsx → about/page.tsx} +3 -3
  96. package/templates/apps/frontend-i18n/src/pages/[locale]/mirrors.test.tsx +2 -2
  97. package/templates/apps/frontend-i18n/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  98. package/templates/apps/frontend-i18n/src/pages/{about.page.test.tsx → about/page.test.tsx} +3 -3
  99. package/templates/apps/frontend-i18n/src/pages/{about.page.tsx → about/page.tsx} +1 -1
  100. package/templates/apps/frontend-i18n/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  101. package/templates/apps/frontend-landing/package.json +7 -7
  102. package/templates/apps/frontend-landing/src/pages/[locale]/mirrors.test.tsx +1 -1
  103. package/templates/apps/frontend-landing/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  104. package/templates/apps/frontend-landing/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  105. package/templates/apps/frontend-spa/package.json +7 -7
  106. package/templates/apps/frontend-spa/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  107. package/templates/apps/frontend-ssr/package.json +7 -7
  108. package/templates/apps/frontend-ssr/src/pages/{feed.page.test.tsx → feed/page.test.tsx} +2 -2
  109. package/templates/apps/frontend-ssr/src/pages/{feed.page.tsx → feed/page.tsx} +1 -1
  110. package/templates/apps/frontend-ssr/src/pages/{feed-swr.page.test.tsx → feed-swr/page.test.tsx} +2 -2
  111. package/templates/apps/frontend-ssr/src/pages/{feed-swr.page.tsx → feed-swr/page.tsx} +1 -1
  112. package/templates/apps/frontend-ssr/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  113. package/templates/apps/frontend-ssr-api/package.json +8 -8
  114. package/templates/apps/frontend-ssr-api/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  115. package/templates/apps/frontend-static-blog/package.json +6 -6
  116. package/templates/apps/frontend-static-blog/src/pages/[locale]/blog/{[slug].page.tsx → [slug]/page.tsx} +4 -4
  117. package/templates/apps/frontend-static-blog/src/pages/[locale]/mirrors.test.tsx +2 -2
  118. package/templates/apps/frontend-static-blog/src/pages/[locale]/{index.page.tsx → page.tsx} +2 -2
  119. package/templates/apps/frontend-static-blog/src/pages/blog/{[slug].page.test.tsx → [slug]/page.test.tsx} +3 -3
  120. package/templates/apps/frontend-static-blog/src/pages/blog/{[slug].page.tsx → [slug]/page.tsx} +3 -3
  121. package/templates/apps/frontend-static-blog/src/pages/{index.page.test.tsx → page.test.tsx} +1 -1
  122. package/dist/apiBuild-BOczx9-m.js +0 -2
  123. package/dist/dbCommand-DNQ6l-su.js +0 -2
  124. package/dist/frameworkTableAssembly-D1zJSgOU.js +0 -2
  125. /package/templates/apps/changelog/src/pages/{index.page.tsx → page.tsx} +0 -0
  126. /package/templates/apps/frontend-admin/src/pages/(marketing)/{index.page.tsx → page.tsx} +0 -0
  127. /package/templates/apps/frontend-admin/src/pages/admin/{index.page.tsx → page.tsx} +0 -0
  128. /package/templates/apps/frontend-app/src/pages/{index.page.tsx → page.tsx} +0 -0
  129. /package/templates/apps/frontend-blank/src/pages/{index.page.tsx → page.tsx} +0 -0
  130. /package/templates/apps/frontend-contact/src/pages/{index.page.tsx → page.tsx} +0 -0
  131. /package/templates/apps/frontend-dashboard/src/pages/(marketing)/{index.page.tsx → page.tsx} +0 -0
  132. /package/templates/apps/frontend-dashboard/src/pages/dashboard/{index.page.tsx → page.tsx} +0 -0
  133. /package/templates/apps/frontend-docs/src/pages/{index.page.tsx → page.tsx} +0 -0
  134. /package/templates/apps/frontend-i18n/src/pages/{index.page.tsx → page.tsx} +0 -0
  135. /package/templates/apps/frontend-landing/src/pages/{index.page.tsx → page.tsx} +0 -0
  136. /package/templates/apps/frontend-spa/src/pages/{index.page.tsx → page.tsx} +0 -0
  137. /package/templates/apps/frontend-ssr/src/pages/{index.page.tsx → page.tsx} +0 -0
  138. /package/templates/apps/frontend-ssr-api/src/pages/{index.page.tsx → page.tsx} +0 -0
  139. /package/templates/apps/frontend-static-blog/src/pages/{index.page.tsx → page.tsx} +0 -0
@@ -1,4 +1,4 @@
1
- # What's new in 0.15.0
1
+ # What's new in 0.18.0
2
2
 
3
3
  Read this FIRST when a task touches an area you have not worked in recently.
4
4
  It is the cheapest way to notice that the framework grew the thing you were
@@ -9,123 +9,228 @@ BREAKING entries name a codemod; run `voltro update` to apply it.
9
9
 
10
10
  ### ⚠ BREAKING
11
11
 
12
- - **@voltro/cli** — **A rename now carries the ALIASED importers, not only the relative ones.**
12
+ - **@voltro/database** — **`_internalCmp` is no longer exported from `@voltro/database/sql`.**
13
13
 
14
- An app's web build stopped compiling after `voltro update`: 84 relative imports were rewritten correctly, 163 aliased ones across 88 files were not, and `tsc` reported 249 errors on names that no longer existed. Nothing in the codemod's output hinted that a whole class of import had been skipped.
14
+ It was re-exported from the migration planner with the comment "exported for tests that want a custom column sort". No test ever imported it — not in `@voltro/database`, not in another package, not in any sibling repo. It was published surface that existed for nobody, and it stayed invisible because the API goldens covered only each package's main entry until now.
15
15
 
16
- Two independent halves, and each alone leaves the imports stale:
16
+ `codemod: none` because nothing can plausibly be importing it: a grep of every workspace package and every sibling repo finds the export line and no call site. `cmp` remains the local helper it always was; if a test genuinely needs it, export it again together with the caller that justifies it.
17
+ - **@voltro/database, @voltro/runtime** — **`.where(col, 'like', value)` is removed, and `startsWith` takes the job it was pretending to do.**
17
18
 
18
- - The codemod's ts-morph project was built with **no `baseUrl` and no `paths`**, so `@/components/link` resolved to nothing. It now gets the compiler's own shape the raw `paths`, not the pre-resolved Vite table, because a `paths` target is relative to `baseUrl` by definition and an absolute one does not resolve. - `SourceFile.move()` rewrites relative specifiers and **nothing else**, which is correct on its own terms: ts-morph cannot know whether the alias mapping or the file is meant to change. So the codemods now rewrite the aliased ones themselves, narrowly — only specifiers that RESOLVED to the moved file, and only the trailing stem, which needs no alias table and so cannot disagree with one. The run reports how many it rewrote.
19
+ `'like'` never behaved like SQL LIKE. It mapped to `contains``%…%` around the value, case folded on both sides so `.where('path', 'like', '/api/%')` matched only rows literally containing the characters `/api/%`, and the wildcard the caller wrote did nothing. The docs advertised `.where('col', 'like', 'abc%')`, which is exactly the shape that silently returns nothing.
19
20
 
20
- **A file an exact `paths` entry names is left alone and reported.** Renaming `link.tsx` while `"@/link": ["src/components/link.tsx"]` points at it leaves the mapping resolving to nothing — and the same path is usually repeated in a vite/vitest alias table no codemod owns. One app hit this and then saw a taxonomy violation reported on a file the codemod had itself created.
21
+ In its place, a real prefix predicate:
21
22
 
22
- **`page/unsuffixed-in-pages` is now an error, not a warning.** The comment justifying the warning contradicted the scanner it described: that bucket is already narrowed to a default export nothing imports, which is what an unmigrated page looks like and what a co-located component never does. The failure it names is invisible everywhere else — an unmigrated route simply 404s, with a clean `tsc` and a green suite. One app finished a migration with 51 of them. `codemod: none`: the rename it asks for already ships as `0.14.0/03_pages-suffix`, and nothing about a user's SOURCE changes here — what changes is that `voltro check` now fails on a route that does not route.
23
- - **@voltro/protocol, @voltro/client, @voltro/web** **`errorTag` moves from `@voltro/client` to `@voltro/protocol`.**
24
-
25
- It reads the `_tag` that `toRpc` writes, so it now lives beside `toRpc` — one file owning both ends of that contract. Where it used to live had a cost invisible from inside the framework: an app's shared error handler sat in a package that pulled only `@voltro/i18n`, and reading a tag would have meant depending on the entire client package for seven lines. They declined, and kept parsing message strings with a regex — the exact outcome the helper exists to prevent.
23
+ ```ts
24
+ .where('key', 'startsWith', 'awb_') // ergonomic form
25
+ where(startsWith('key', 'awb_')) // predicate helper
26
+ jsonField('config', 'ns').startsWith('awb_') // inside a json() column
27
+ ```
26
28
 
27
- `@voltro/web` re-exports the client surface, so it loses the symbol toothe same codemod covers an app that imported it from there.
29
+ `startsWith` is **case-SENSITIVE**, unlike `contains`, and the asymmetry is the design rather than an oversight. `contains` is a search primitive a human typing into a box means `hello` to find `Hello`. A prefix is a NAMESPACE: `awb_` and `AWB_` are two different key spaces, and quietly merging them is a bug. It also matches the JS method it is named after.
28
30
 
29
- Not re-exported from `@voltro/client`: two import paths for one helper is how the next reader learns the wrong one. The transform codemod repoints the import, preserving an alias (`errorTag as tagOf`) and the type-only form, and merges into an existing `@voltro/protocol` import rather than adding a second.
31
+ It is the one string predicate a database can answer from an index: it lowers to `LIKE 'literal%'`, which a btree can range-scan. `contains` (`%…%`) cannot, which is also why there is no `endsWith` a second un-indexable operator would only look cheaper than it is. `%` and `_` in the value are escaped, so they match literally, and the ESCAPE clause is stated per dialect (sqlite and mssql have no default escape character; mysql already has backslash *and* treats it as a string escape, so the clause is omitted there).
30
32
 
31
- While moving it, its doc comment gained the thing that matters at the call site and was only implied before: **`instanceof` does not hold on the client.** What arrives there was decoded from JSON and never constructed, so match on the tag, not on the class. One team read the old wording as a promise that `instanceof` works and was right to say so.
33
+ **Migration** is deliberately manual. Rewriting `'like'` `'contains'` would reproduce exactly what runs today, bug included, and report the migration as complete while every site that used a wildcard keeps returning nothing. Each call site is one of two things and only its author can tell which: a wildcard pattern (→ `startsWith`, and that query has been wrong until now) or a substring search spelled oddly (→ `contains`, no behaviour change).
32
34
 
33
35
  ### Added
34
36
 
35
- - **@voltro/protocol, @voltro/cli** — **An auth strategy reaches the app's DataStore, on `input.store`.**
37
+ - **@voltro/protocol** — **`apiKeyStrategy`'s `resolveKey` receives the strategy input** the same object a hand-written `AuthStrategy.resolve` gets, as a second argument:
36
38
 
37
39
  ```ts
38
- const sessionStrategy: AuthStrategy = {
39
- id: 'db-session',
40
- resolve: async ({ headers, store }) => {
41
- if (store === undefined) return { kind: 'skip' } // still booting
42
- const [row] = await store.query(sessions.byToken(headers.authorization))
43
- return row ? { kind: 'matched', subject: toSubject(row) } : { kind: 'skip' }
40
+ apiKeyStrategy({
41
+ prefix: 'awb_',
42
+ resolveKey: async (hash, { store }) => {
43
+ const rows = await store?.query(apiKeys.byHash(hash))
44
+ return (rows?.[0] as ApiKeyRecord | undefined) ?? null
44
45
  },
45
- }
46
+ })
46
47
  ```
47
48
 
48
- Without it, a DB-backed strategy a session row, an API-key record, a PAT table had to open a SECOND connection path beside the framework's, to the same database the request store opens a moment later. One adopter's `auth/db.ts` is 105 lines of exactly that: a second `ManagedRuntime` plus a `MysqlClient`, load-bearing for their session lookup and their ApiKeyStore. Every DB-backed OIDC / SAML / PAT integration rebuilds it, which is what made this a framework gap rather than an app's problem.
49
+ `AuthStrategyInput.store` shipped two releases ago and this helper was the one auth seam that could not reach it: an app using `apiKeyStrategy` had to wrap it in its own strategy purely to close over a store the framework had already handed over or keep the second connection path to the same database that the seam exists to delete. Existing one-argument resolvers are unaffected.
50
+ - **@voltro/testing** — **`describeIfReachable` — because "the database was not there" must not look like "the database was fine".**
51
+
52
+ Every integration and dialect-parity suite probes a TCP port and bails when the service is down. The bail was hand-written each time, and the hand-written form ends a test body with an early `return` — which is a PASS. So with no database at all, a suite reports `Tests 2 passed`. The only trace is a smaller duration, which nobody reads, and vitest swallows the accompanying `console.warn` by default, so even the intended signal never prints.
53
+
54
+ Found by running the postgres introspection suite against port 1: `2 passed`, having connected to nothing. That suite guards the fix for a boot hang, so a green run there was load-bearing evidence that meant nothing.
55
+
56
+ `describeIfReachable(label, target, suite)` makes the honest outcome the default: an unreachable target produces a vitest SKIP — reported as `skipped`, counted separately, with the missing service named in the suite label. "We did not verify this" and "we verified this and it holds" no longer print the same.
57
+
58
+ Same class as the changelog and message-API selftests: a check that has quietly stopped checking still prints green, and green is read as evidence.
49
59
 
50
- It is the **same value** `auth.resolveScopes` already receives, through the **same lazy getter** one ref, two consumers, rather than each caller reaching for the store its own way. That is deliberate: `voltro dev` builds the store AFTER the auth chain and `voltro serve` builds it BEFORE, so a value captured at config time would be `undefined` forever in dev and correct in production. The getter is read ONCE per request, not once per strategy.
60
+ **All 36 suites carrying that shape are converted** every one was fully gated, so wrapping the suite loses no test. Verified in both directions, because only one of them is obvious:
51
61
 
52
- `store` is `undefined` only while the store is still being built, and on an app with no store a strategy should `skip` rather than throw. It is the BOOT store, not a request-scoped one: strategies resolve before a request store exists.
62
+ | | before | after | |---|---|---| | no services running | 105 tests **passed** | **0 passed, 92 skipped** | | the full stack up | 105 passed | **all pass, 0 undeclared skips** |
53
63
 
54
- **Not narrowed to a read-only surface**, and the reason is worth stating: the narrower type would be the better guarantee, `DataStore` is the driver SPI, and giving strategies a different type from the one `resolveScopes` gets would put two views of one object in the same file. A strategy that writes during subject resolution is a design mistake; the type system is not going to catch it for you. Read users / sessions / keys, do not run domain writes.
64
+ The second row is the point the sweep did not quietly turn a suite off.
65
+
66
+ **It paid for itself immediately, three times.**
67
+
68
+ *Twelve replication tests had never run.* postgres streaming replica, mysql GTID replica, mssql Always-On AG — left out of CI on the grounds that starting them OOMs a standard runner, so they reported `passed` in every run without once executing. Replication and failover: precisely the behaviour nobody can verify by reading it. Measured rather than argued: baseline 2091 MiB, `postgres-replica` **82**, the mysql pair **1335**, the AG pair **2035**. The OOM claim is true of the WHOLE compose file (keydb, dragonfly, valkey, redis cluster) and not of these. `ci.yml` now starts them — plus the two one-shot containers that actually FORM the availability group, without which both nodes are healthy and the suite still cannot connect.
69
+
70
+ *Eighteen cache tests had tested one engine out of four.* The RESP suite iterates redis / valkey / keydb / dragonfly; only redis was started. The other three cost **27 MiB between them**.
71
+
72
+ *Three SQL Server instances were fighting over memory.* With the AG nodes running beside `mssql-test`, two mssql round-trip suites failed — and passed when run alone, which is how they would have been dismissed as flakes. Each instance now declares `MSSQL_MEMORY_LIMIT_MB`, so the stack is the same size on a laptop and on a runner. Under the full gate, with every package's suite running in parallel, sql-mssql is 59/59.
73
+
74
+ **What remains skipped is skipped for a reason, and the reason is written down.** Four tests, in `sql-sqlite` and `sql-turso`: `clusterTestSuite` gates cross-process resume on `clusterResume`, which needs the workflow runner's state in SQL, and neither dialect keeps it there. Not a missing service — a capability that does not exist for that dialect. That distinction is the whole content of the allowlist.
55
75
 
56
76
  ### Fixed
57
77
 
58
- - **@voltro/cli** — `voltro dev` terminates when its dev server does, instead of living forever.
78
+ - **@voltro/runtime, @voltro/cli, @voltro/protocol** — **The boot store (`AuthStrategyInput.store`, `PluginHttpRouteRequest.store`) applies the storage codec.** `.encrypted()` columns decrypt on read and encrypt on write, and array columns round-trip on dialects with no native array type.
79
+
80
+ Both seams are documented as "not tenant-scoped", which is correct and unavoidable: they hand out a store to code that runs BEFORE a Subject exists, so tenant scope, soft-delete filtering, audit stamping and row-level security genuinely cannot apply. What that was silently taken to mean is "the raw driver", and it dropped two things that need no Subject at all.
81
+
82
+ The consequence was a silent wrong answer rather than an error. An auth strategy reading an `.encrypted()` column got the literal string `enc:v1:…` back — which compares, concatenates, renders and logs perfectly well, and simply never matches the token it is compared to. The failure surfaces as "wrong credential". On the write side it was worse and unrecoverable: an insert through that store wrote PLAINTEXT into a column the schema declares encrypted.
83
+
84
+ The line is now **everything that does not need a Subject**, not "less than `ctx.store`". Wired in `voltro dev` and `voltro serve` in the same change (`wrapStoreWithBootCodec`); `raw()` is deliberately left as a pass-through, since it is the documented escape hatch for hand-written SQL and re-encoding rows a caller asked for verbatim would be its own surprise.
85
+ - **@voltro/cli** — **The remaining file-moving codemods narrow their projects too**, and the shared project is no longer a stale snapshot.
86
+
87
+ `0.14.0/03_pages-suffix` is gated on `/src/pages/` throughout — plus each app's `app.config.ts`, which is how it tells a real web app's pages from a library that merely keeps components under `src/pages/`. `0.15.0/01_undo-mistaken-taxonomy-renames` only ever renames `*.component[.ui].ts(x)` and an export-less `*.types.ts(x)`. Both now declare that scope and take the importer closure instead of the workspace.
88
+
89
+ `0.14.0/04_file-taxonomy` deliberately keeps the whole project: its `isCandidate` is "any `.ts`/`.tsx` that does not already carry a convention", so there is nothing to narrow and pretending otherwise would only move the cost.
90
+
91
+ **And a real bug the equivalence test caught.** The shared project was built once, lazily, at the first whole-project codemod — *before* a narrowed one wrote its renames to disk. So `0.14.0/04` ran against the pre-`03_pages-suffix` tree and classified `alpha.tsx` as a component, where the whole-project run produced `alpha.page.tsx`. The shared project is now dropped whenever a narrowed codemod changes the disk, and each shared codemod saves before the next narrowed one reads it — the disk is the single source of truth in both directions.
92
+
93
+ Nothing in either codemod's own output hinted at it: both reported success, with different results. Verified by running the same fixture through the 0.13.0 → 0.16.0 jump both ways and diffing the trees; identical, including an aliased and a relative importer outside every scope.
94
+ - **@voltro/cli** — **`voltro update` sizes the codemod pass's heap from the machine.** Node caps the old space near 4 GB regardless of installed RAM, so a large monorepo died on a machine with memory to spare — and died in V8, with no line naming a limit.
95
+
96
+ The pass holds one ts-morph project whose cost is roughly linear in the file count: measured at ~97 KB per file on a synthetic fixture (818 MB at 2,320 files, 1,399 MB at 8,320 — median of three runs on an otherwise idle machine). An adopter's repo needed ~30 GB; at node's default it aborted in 83 seconds.
97
+
98
+ The re-exec'd child now gets `--max-old-space-size` at 75% of total RAM, leaving room for the OS. An explicit `NODE_OPTIONS` from the caller always wins, and nothing is set when 75% would be *below* node's own default — a lower ceiling than node would pick is a pure regression.
99
+
100
+ This does not make an impossible run possible; it stops an arbitrary limit from being the binding one. Where the machine genuinely lacks the memory, the scan's file ceiling reports it in words.
101
+
102
+ **Measured, not assumed — and three plausible fixes were measured and rejected first**: replacing the runner's per-codemod full-text bookkeeping, releasing ts-morph's node-wrapper cache, and hoisting the aliased-importer resolution out of the move loop. A fourth, chunking the pass into per-codemod projects, is correct (109/109 codemod tests pass with one file per project) but does not flatten the curve — peak still grows ~97 KB per file either way, because the codemods that move files must see the importers and so keep the whole-project view.
103
+ - **@voltro/cli** — **A codemod that moves files no longer loads the whole workspace.** Peak memory now follows what the codemod touches instead of how large the user's repository is.
104
+
105
+ A mover has to see its importers — `SourceFile.move()` rewrites the relative specifiers pointing at the moved file and `moveCarryingAliasedImports` does the same for aliased ones, but only for importers that are IN the project. So the movers took everything, and everything is what made the pass cost ~107 KB per file. An adopter's monorepo needed ~30 GB and died in a V8 abort.
106
+
107
+ `codemodImporterClosure` answers "who imports these" from TEXT: one streaming pass that extracts module specifiers and discards the source. A codemod declaring `scope` + `needsImporters` then gets a project of its scope plus that closure. `0.17.0/01_pages-are-directories` — the most expensive codemod in a run — is the first to use it; it only ever touches files under `src/pages/`, so the rest of the repo was pure cost.
108
+
109
+ Measured on the 0.16.0 → 0.17.0 jump, 300 pages, synthetic fixture:
59
110
 
60
- The supervisor never watched its child die. `runChild` was an `Effect.acquireUseRelease` whose `use` was `Effect.never`, so `proc.on('exit')` existed ONLY in the release path and that path runs when the fiber is interrupted (a restart, a signal), never when the child exits by itself. Two more places assumed the same thing: `runSupervisor` was typed `Effect<never>`, and `dev.ts` returned `new Promise(() => {})` after starting it.
111
+ | files | narrowed | whole project | |---|---|---| | 2,320 | 554 MB | 720 MB | | 8,320 | **617 MB** | 1,359 MB |
61
112
 
62
- So a boot that aborted an unreachable database, a refused migration, a failed env gate left the child dead and the supervisor waiting for a file change that nobody was there to make. Measured, not inferred: one developer machine carried 15 such `voltro dev` process pairs, `ppid=1`, the oldest 7 days old, every one a boot that had failed against a remote database. They hold a watcher and a terminal-less process each; in CI the same shape keeps a runner busy after the job "finished".
113
+ ~10 KB per file instead of ~107 KB — the curve is flat, which is the point: the run no longer gets harder because the repository grew.
63
114
 
64
- `use` now awaits the child (`awaitChildExit`) and the supervisor races that against the watch loop, so whichever happens first decides. A restart still does NOT end it `stopChild` interrupts the fiber, so the deferred is never completed on that path. A child killed by a signal reports `code: null`, which is reported as a failure rather than a clean 0.
115
+ **The closure's matching rule is the same one `rewriteAliasSpecifier` applies**, so a file it leaves out is one the mover would not have rewritten anyway. Directory names are indexed too, because `import x from './settings'` resolving to `settings/index.page.tsx` names the directory and never the stem and `move()` rewrites that specifier.
65
116
 
66
- What a self-exit MEANS then depends on whether anyone is watching, because the two failure modes pull in opposite directions:
117
+ Verified by equivalence, not by inspection: the same fixture run both ways produces byte-identical trees, including an aliased and a relative importer living OUTSIDE the codemod's scope. The first version of this failed that test — the specifier index was built over the union of the selected scopes, which no longer contains the importers once a codemod narrows its own.
118
+ - **@voltro/cli** — **The codemod scan's file ceiling now applies to the git enumeration path**, which is the path every real project takes.
67
119
 
68
- - **Interactive** (stdout is a TTY) a crashed boot is something you are about to fix, so the supervisor says so and keeps watching. The next save restarts it, which is what every other dev server does; stopping would throw away the watcher mid-edit and make you retype the command. - **Non-interactive** nobody is going to fix anything. `voltro dev` exits with the child's code, so a failed boot is a failed command. This is the case that produced the invisible processes, and the one CI actually waits on.
120
+ `enumerateScopedFiles` has two branches: `git ls-files` when the root is a repository, and a glob walk otherwise. The ceiling with the error that explains what to exclude sat in the glob branch only. So a git repository walked straight past it, loaded the whole workspace into one ts-morph project, and died in V8 with no line naming a heap. An adopter measured it: 4 GB aborts in 83 s, 24 GB after 14 minutes, 30 GB completes in ~9 minutes. "It died" was the entire diagnosis available to them from a guard written to prevent exactly that.
69
121
 
70
- A CLEAN exit always stops, watched or not. `VOLTRO_DEV_KEEP_ALIVE=1|0` forces the answer for what the TTY check cannot see a CI runner with a TTY allocated, or a wrapper that pipes output while a human still watches it and cannot keep a clean exit alive, which would turn a deliberate shutdown into a hang.
122
+ The message now also says WHY the count matters (every matched file is parsed into one project), and names three ways out in order: `.gitignore` for non-source trees, running from the app directory, or `--only <id>` one codemod at a time. `VOLTRO_CODEMOD_MAX_FILES` raises the ceiling for a workspace where the whole set really is source, alongside `NODE_OPTIONS=--max-old-space-size`. The limit is read at call time rather than frozen at import.
71
123
 
72
- Pinned against REAL child processes, because the defect was an Effect that never settled a stubbed `once('exit')` that resolves is exactly what would have passed while the bug shipped.
73
- - **@voltro/database** — **A user-facing message that names an API must have one now checked in CI.**
124
+ Regression cover asserts BOTH branches, and the git one was verified by removing the call and watching that test go red.
125
+ - **@voltro/cli** — **`component/one-per-file` counted things that are not components and its message asserted they were, which is what made it expensive.**
74
126
 
75
- The sibling of the claimed-wiring check. That one asserts a doc comment's claimed caller exists; this one asserts a message's claimed API exists. Same failure shape, worse audience: a doc comment is read by somebody browsing, a refusal by somebody already blocked and looking for the sanctioned way out.
127
+ The classifier read the first letter of the exported NAME. So two shapes reported clean code as broken, both found by an app adopting the taxonomy across 658 files:
76
128
 
77
- It exists for a reported bug that nothing could have caught. The drop-table refusal offered, as its FIRST option, *"chain `.dropped()` on it"* tables have no such marker, only columns do. Doc SAMPLES are typechecked; message strings are not, and cannot be. Three of one release's reported defects lived in that blind spot.
129
+ - `export const DEFAULTS = { a: 1 }` beside a component was reported as `exports 2 unrelated components: DEFAULTS, OnlyOne`. A plain object, named as a component. That app split a five-line constant into its own file to satisfy a rule that was misfiring. - `export default OnlyOne` next to `export const OnlyOne` was reported as two components, the second one named `Default`. One binding, exported twice the export FORM was being counted, not the component.
78
130
 
79
- Three rules, each with an unambiguous answer, because a noisy gate is skipped and then costs more than it saves:
131
+ Classification now comes from the DECLARATION: a function, a class, an `FC`-annotated binding, a `memo`/`forwardRef`/`observer` wrapper, a tagged template. An object, an array, a string, a number, a `new` — not components. The `default` entry resolves to the declaration it names and de-duplicates on the node, so the same component cannot be counted once per export form.
80
132
 
81
- - a `VOLTRO_*` variable a message tells you to **set** must be read somewhere, - a `` `.method()` `` a message tells you to **chain** must be a callable MEMBER of a published type, - a `--flag` in a `voltro …` instruction must be parsed.
133
+ The classifier is deliberately GENEROUS about the unknown, because the two error directions are not symmetric: calling a component "not a component" makes the rule report `exports no component` on a correct file, which is worse than letting one unusual value through.
82
134
 
83
- The member rule is the one that took two attempts. The first version asked "does this name exist in the public surface" and the motivating bug **passed it**: `dropped` is exported, as a free `dropped()` you write as a column's value. A dotted claim is a claim about something chainable, so a free function and a `readonly dropped?: boolean` data property are both correctly rejected now.
135
+ The catalogue said a `*.component.tsx` promises "exactly one component **(+ types)**", which reads as "types are the only exception". It never was the rule counts components, and a `const COLUMNS = […]` beside the table that renders it was always allowed. The docs now say so in both languages.
136
+ - **@voltro/cli** — **`voltro dev` could not start at all in a strict-pnpm install: the supervisor respawned with a bare `--import tsx`.**
84
137
 
85
- **It immediately found a second instance nobody had reported** the drop-COLUMN refusal also said "chain `.dropped()`", one level down from the reported one, and the real spelling is `<column>: dropped()` as the field's value. Close enough to guess from, which is why it survived.
138
+ Node resolves a bare specifier against the CHILD's working directory. `@voltro/cli` declares tsx; the user's app does not — so under a non-hoisting layout tsx lives inside `node_modules/.pnpm/@voltro+cli@…/node_modules/tsx` and is invisible from the app root. The first respawn died with `Cannot find package 'tsx'`, naming a package the reader never asked for and cannot usefully install.
86
139
 
87
- Ships with a `--selftest` that runs first in CI, for the reason the changelog gate has one: a check that has quietly stopped detecting anything still prints green, and green is read as evidence.
88
- - **@voltro/database** — **Two migration refusals sent people the wrong way** — the worst place for a bad hint, because whoever reads one is already blocked and looking for the sanctioned way out.
140
+ `bin/voltro.mjs` had already learned this and resolves an ABSOLUTE URL before spawning. The supervisor then threw that answer away and re-derived a worse one.
89
141
 
90
- **The drop-table refusal recommended an API that does not exist.** Its first option was *"add it to your declared set + chain `.dropped()` on it"*. There is no table-level `dropped()` only the column marker. The recommendation was also the conceptually RIGHT one, which is what made it expensive: the two options that do work are both worse, so a reader picks the one they cannot follow.
142
+ Every spawn site now goes through `tsxLoaderArgs()`, which prefers the loader already present in `process.execArgv` — literally the absolute path the shim computed, and inheriting it also preserves the user's own node flags (`--inspect`, `--max-old-space-size`), which a hand-built flag list silently dropped. Behind that: `VOLTRO_TSX_IMPORT`, now exported by the shim for grandchildren that are not themselves loader-registered; then resolution from `@voltro/cli` itself, the package that depends on tsx. The bare specifier survives only as a last resort, and it now says so instead of failing mutely.
91
143
 
92
- There is now a real per-table answer: **`VOLTRO_DESTRUCTIVE_OK` accepts a table list**, not just `1`. `VOLTRO_DESTRUCTIVE_OK=old_things` acknowledges the data loss for that table and leaves every other lossy op in the plan blocked. `1` still means all of them which is rarely what somebody means, and was previously the only way to say anything. A user with one intended drop and three other lossy ops had to acknowledge all four or hand-write a `DROP TABLE` migration, the path 0.14.0's own upgrade note warns against.
144
+ Three of the five spawn sites were already correct `envWatch`, the web-dev respawn, and the shim — and nothing said the other two were wrong. A test now fails if any non-test file under `src/` writes the flags by hand.
145
+ - **@voltro/cli, @voltro/devtools** — **The in-page devtools overlay had no way to authenticate against a fail-closed inspect surface — and the one channel it documented was disabled two directories away.**
93
146
 
94
- The message also states why there is deliberately no table marker: a dropped COLUMN leaves a slot worth documenting in the declaration; a dropped TABLE leaves nothing, so the marker would be a dead entry you must remember to delete.
147
+ Since `/_voltro/inspect/*` went fail-closed, the overlay's Traces / Webhooks / Indexes panels needed a bearer token. A browser cannot be given one: the only channel that would reach it is a `VITE_`-prefixed env var, i.e. a live credential compiled into every bundle, which this framework refuses to do anywhere. So the overlay pointed at `VITE_VOLTRO_INSPECT_TOKEN` — which `voltro dev` and `voltro build` both make unreadable on purpose, by setting vite's `envPrefix` to a sentinel that matches no real variable. A reader who followed the docs set the variable, got no header, and had no way to see why.
95
148
 
96
- **The drop-column refusal never mentioned `renamedFrom`.** It offered "chain `.dropped()`" or "restore the field" — and followed literally on a rename, the first costs exactly the data the user was trying to keep. When the plan drops AND adds columns on the same table, the message now leads with *"did you rename one?"* and names both sides. The evidence was in the plan the whole time.
149
+ Three changes, one shape:
97
150
 
98
- Finding that required fixing a second thing: the footer read only the BLOCKED operations, and an `add-column` is `safe`. The counterpart of a rename was never in the list it was looking at.
99
- - **@voltro/cli** — **0.14.0's taxonomy codemod renamed two kinds of file it should not have, and a repo that already upgraded carries the damage with a green build.** Both were found by adopters running it on real projects; both are silent — the rename succeeds, the imports are rewritten, nothing throws.
151
+ - **`voltro dev`'s vite proxy attaches the minted bearer server-side** on the `/_voltro/api/<name>` route the panels fetch through. Nothing to configure, and the token never reaches the page. It refuses two cases deliberately: a caller that already sent an `Authorization` header (the dashboard forwards a real one — overwriting it would re-scope somebody else's request), and a non-loopback target (`proxyTarget` is user config, so injecting unconditionally would hand this machine's credential to a host we do not control). - **The dead `VITE_VOLTRO_INSPECT_TOKEN` fallback is gone.** `<VoltroDevtools inspectToken>` remains for reaching an api the proxy does not front, and its doc now says plainly that whatever you pass ships in the bundle. Its test previously admitted, in a comment, that it asserted the null path and called it the fallback — which is how a documented-but-dead channel survived a green suite. - **The empty-state copy said inspect was "open in dev mode".** That stopped being true when the surface went fail-closed, so a reader who hit a 401 was told by the panel itself that it could not have been an auth failure. It now names the remedy.
100
152
 
101
- **A framework primitive was treated as an undeclared file.** `health.route.tsx` `health.route.component.tsx`, five times in one app. The codemod kept its OWN list of "suffixes that already carry a contract" instead of reading `fileConventions.ts`, and `.route.` was not on it the exact drift that module exists to prevent, reproduced inside a file that imports from it. The list is gone; the registry answers now, and it gained `ROUTE_PATTERN` plus a `carriesFrameworkConvention()` every consumer shares.
153
+ Separately: the `indexes` tab-badge count polled with the overlay CLOSED. Its two neighbours (`traces`, `webhooks`) take an `enabled` flag; this one was missed, and it is the expensive one it holds an `EventSource` open per api for the whole life of the page, plus a fallback poll whenever that stream errors.
154
+ - **@voltro/cli** — **`voltro doctor`'s boundary rules could not follow a `@/`-aliased import, so they under-reported — silently.**
102
155
 
103
- The root cause underneath was worse than a missing entry: `export default defineRestRoute({...})` resolved to the placeholder name `Default`, which starts with a capital, and was counted as a COMPONENT on that basis. A default export is now only evidence of a component when the exported thing is callable so a convention nobody has registered yet is safe too.
156
+ The file-taxonomy walker resolved relative specifiers and nothing else. On an app that imports through a tsconfig `paths` alias which is most of them every such edge was invisible, and a rule that cannot see an edge cannot fire on it:
104
157
 
105
- **A file with no exports was called a type file.** `test-setup.ts` `test-setup.types.ts`, while `vitest.config.ts` still named `./test-setup.ts` as a **string**. Not an import, so nothing rewrote it and nothing failed: that suite would have run without its setup and stayed green. Five more went the same way a registry module, two migration runners, a `.register.ts`, and a code generator whose `export` tokens live inside template strings.
158
+ - `internal/foreign-import` and `fixture/production-import` came back CLEAN on code that violates them. That is the dangerous direction: no findings reads exactly like no problems. - `ui/unlinked` fired on all 16 of one app's presentational components, because each was reached only through `@/components/…`. A rule that fires on correct code teaches people to ignore it.
106
159
 
107
- `*.types.ts` promises "zero runtime exports", and that promise only means something for a file that exports TYPES. Zero of everything promises nothing, and renames a module whose whole purpose is being imported for effect where the filename is often the only reference there is. It now requires at least one exported type.
160
+ The graph now resolves aliases from the NEAREST `tsconfig.json` walking up to the scan root `voltro doctor` runs at the project root while `@/*` is declared per app, so reading only the root config found no `paths` at all in the layout we scaffold.
108
161
 
109
- **The shipped codemod undoes both**, and can only reach files whose content proves the suffix was wrong: a `.component.` on a name that already carries a framework convention, and a `.types.` on a file that exports nothing at all. It also prints the one thing it cannot fix references by PATH rather than by import (a vitest `setupFiles`, a tsconfig `include`, a Docker `COPY`) were strings on the way out and are strings on the way back.
110
- - **@voltro/cli** — Three ways `voltro update` failed on a real adopter's host, none of which we could have found ourselves each needs a machine we do not have.
162
+ Two more edge forms were missing for the same reason: `export from` and dynamic `import()`. The re-export one is not a completeness flourish a barrel is the file most likely to reach across a feature boundary, so `export { x } from './orders.internal'` is precisely the case `internal/foreign-import` exists to catch, and it was the one shape the rule could not see.
163
+ - **@voltro/cli** — **The release gate now fails on a skipped test it was not told about.** "All green" has to mean everything RAN, or the total is a number about how little was attempted.
164
+
165
+ Locally a skip stays fine — nobody should need six databases to run `pnpm test`, and a suite that fails without them stops being run at all. In CI it is not fine: a skipped test is an unverified claim wearing the same colour as a verified one.
166
+
167
+ `scripts/check-no-skipped-tests.mjs` reads the per-package vitest summaries out of the test step and fails on anything skipped that is not declared in its `ALLOWED` map with a reason and an **exact** count. Both directions are enforced, and the second is the one that matters:
168
+
169
+ - more skips than declared → something stopped running; - **fewer** skips than declared → the entry is stale, and a stale allowlist silently absorbs the next regression. That is the failure mode an allowlist has instead of the one it removes, and it is only survivable if the list is forced to stay exact.
170
+
171
+ It runs `--selftest` first, like the changelog and message-API gates, for the same reason: a check that has quietly stopped detecting anything still prints green. Both of its rules were verified by breaking them and watching the selftest go red — the ANSI stripping and the stale-entry direction.
172
+
173
+ **Two kinds of skip exist and only one is a defect.** "The dependency was not there" is a coverage gap — start the service. "This does not apply to this configuration" is correct — declare it. The allowlist holds exactly the second kind: four tests in `sql-sqlite` and `sql-turso` whose dialects keep no workflow-runner state in SQL, so cross-process resume is not a thing they can do. The 12 replication tests that would have been the first entries were the FIRST kind, and cost 3.4 GB on a 16 GB runner — `ci.yml` starts their services instead of declaring them away.
174
+
175
+ The check found both of its first three catches on its own first run: 18 cache tests covering one RESP engine of four, and the two dialect suites above. It also caught itself — it passed in 0.2 s on a run whose test step had aborted after 0.5 s, because an empty log has nothing to complain about. A log with no vitest summaries is now a failure, with its own selftest case.
176
+ - **@voltro/cli** — **Four `voltro dev` inspect endpoints answered 200 to any caller: `traces`, `webhooks`, `analytics`, `aggregates`.**
177
+
178
+ `handleInspectRequest` gates everything that reaches it. The branches in front of it are EARLY RETURNS — they answer and never reach it — so each had to remember to gate itself, and four did not. `traces` is the sharp one: an adopter measured 38 KB of live spans from an unauthenticated `curl`, a request-by-request record of what the process just did. Per the tracing docs those spans also carry `rpc.tag`, `subject.type` and `tenant.id`.
179
+
180
+ That is the split `0.12.0` was written to close — *"the absence of a secret is not consent"* — with `metrics` and `logs` gated and `traces`, which is strictly more revealing than either, not.
181
+
182
+ **Scope, because the reporter could not test it and asked: `voltro serve` and `voltro start` are NOT affected.** Neither mounts these branches — `serveApi.ts` contains no `/_voltro/inspect` path at all, and `start.ts` goes through the shared, gated `handleInspectRequest`. The leak is dev-only, which lowers the severity without removing it: a dev server on a shared machine or a bind-mounted container is not private either.
183
+
184
+ The gate now runs ONCE at the door, before any branch, so a new branch cannot be added without it. CORS preflight stays exempt — a browser sends `OPTIONS` with no `Authorization` header by construction, and the preflight carries no data.
185
+
186
+ **The failure mode was already written down one level below.** `inspectLogsEndpoint` carries the comment *"gating per-caller drifted (start was ungated, dev gated nothing, webDev gated disabled-but-not-token)"* and single-sources the gate inside the handler. The lesson was right; it was applied at the wrong depth.
187
+
188
+ **One branch changed behaviour beyond the four: `POST /_voltro/inspect/clientLog` on the API.** It is an ingest endpoint, so the risk it carried was injection rather than disclosure — anyone could write lines into the developer's terminal log. Nothing in the framework posts there: `@voltro/web`'s browser bridge ships to its own origin, which the web dev server serves and deliberately leaves ungated (a browser has no token, and that path accepts only log batches). The API's copy is reached by direct callers, which can carry one.
189
+
190
+ **The overlay is carried across this**, in the same release: the Vite dev proxy now attaches the minted bearer server-side (see the devtools-overlay entry), so the panels stay live and the browser still never holds the token.
191
+ - **@voltro/database** — **Postgres FK/PK introspection reads `pg_catalog`, not `information_schema` — this killed a `voltro dev` auto-migrate hang that never finished.**
192
+
193
+ This shipped in code without a changelog entry, so nobody upgrading was told either that the hang was fixed or that a new env var exists. Recording it now, with the numbers measured on our own hardware rather than taken from the report.
194
+
195
+ On a FK-dense schema, `voltro dev` hung indefinitely at `auto-migrate: planning schema` — pod 0/1, no error, no timeout. The FK query 4-way-joined `information_schema.{table_constraints, key_column_usage, constraint_column_usage, referential_constraints}`. `constraint_column_usage` is a security-barrier view whose `table_name IN (…)` predicate does **not** push down, so every batch re-scanned FK metadata for the whole catalog. The `blocked` refuse-gate sits *after* introspection, so its helpful message never printed.
196
+
197
+ Measured against a live 532-table / 2637-FK postgres 17:
198
+
199
+ | | one batch of 20 tables | full introspection | |---|---|---| | `information_schema` | **2041 ms** | ~55 s extrapolated over 27 batches | | `pg_catalog` OID join | **8.7 ms** | **404 ms cold, 368–374 ms warm** |
200
+
201
+ Both return the identical 90 rows for that batch, so this is a correctness-preserving rewrite, not a narrowing. `PgFkRow` and `mapPgRule` are unchanged — `confdeltype`/`confupdtype` chars map back to the information_schema rule tokens in SQL. Multi-column FKs pair positionally via `unnest(conkey/confkey) WITH ORDINALITY`; composite PKs keep declared (`conkey`) order.
202
+
203
+ Batching is kept, not removed: it exists for pooler mis-framing of large responses, and with the OID join the batch filter pushes down, so each batch scans only its own tables.
204
+
205
+ **New env var: `VOLTRO_INTROSPECT_TIMEOUT_MS`** (default 30000, `0` disables). The whole postgres introspection runs in one read transaction under `SET LOCAL statement_timeout`, so a query that ever degenerates again dies with an actionable error instead of freezing a pod at 0/1. `SET LOCAL` reverts at commit — no pooler session-state leak.
206
+
207
+ MySQL/MariaDB were never affected: their FK query already joins `key_column_usage` to `referential_constraints` with no `constraint_column_usage` cross-join.
208
+ - **@voltro/cli** — **`voltro update --only <id>` works in the spelling the tool itself prints**, and an unknown flag is now refused instead of ignored.
209
+
210
+ `--only` was read by a separate pass that the positional-argument loop knew nothing about, so the flag was skipped and its VALUE — which does not start with `--` — fell through and was resolved as the app directory. The report named a path the user never typed:
211
+
212
+ ```
213
+ $ voltro update --codemods-only --only 0.17.0/01_pages-are-directories
214
+ voltro update: no package.json at …/web/0.17.0/01_pages-are-directories
215
+ ```
111
216
 
112
- **The install could not run here, and the refusal left the tree half-upgraded.** Their install runs in a container against its own store. `voltro update` ran the package manager on the host anyway; pnpm refused (it wanted to remove `node_modules` and had no TTY to ask) and exited after the version bumps were already written and before any codemod ran. That is the state this command's own documentation calls the worst one to be in, and it was reachable by design.
217
+ Both the usage text and the dirty-tree hint print `--only <id>`, so the documented form was the broken one. `--only=<id>` and `--only <id>` now both work, on `voltro update` and on the `_apply-codemods` entry it re-execs into.
113
218
 
114
- `--no-install` now writes the bump and stops, saying plainly that the tree is half-upgraded and naming both remaining steps. The install-failed message points at it too. Note what this is not: a compatibility flag. It is a mode for a host where the install is somebody else's job, and it ends by telling you the job is not done.
219
+ Separately, an unrecognised flag now exits 1 with `unknown flag <name>`. A typo'd `--codemod-only` (singular) used to be dropped silently and the command then ran the FULL update, bump and install included, on a tree the user had asked to touch as little as possible.
115
220
 
116
- **The codemod scan exhausted a 12 GB heap, and said nothing about why.** The crash was a bare V8 out-of-memory stack. The scan pruned the directories WE know about — `node_modules`, `dist`, `.turbo` — which cannot cover a project's own heavy ignored trees (a build cache, a data dump, a virtualenv).
221
+ ### Internal (no consumer-facing effect)
117
222
 
118
- Inside a git repository the scan now asks git: `git ls-files --cached --others --exclude-standard` is exactly "files this project considers its own", and a codemod rewrites source — source that git ignores is not source we may rewrite. It also removes the traversal, so there is nothing left to exhaust memory on. Outside a repo the walk remains, now with a ceiling that REPORTS which directory to exclude instead of dying namelessly.
223
+ - **The API goldens now cover every published entry point, not just `dist/index.d.ts`.**
119
224
 
120
- **"not a Voltro app" was the wrong conclusion.** Said of a directory containing an `app.config.ts`, it sends the reader looking for the wrong problem. Three web apps in a workspace inherited from another tool had their `@voltro/*` dependencies in an ancestor `package.json` the apps ARE Voltro apps; only the declaration lives elsewhere. With an `app.config.ts` present the message now says that, and names the two ways forward.
225
+ The changelog already states the rule: "The public API of each package is its `publishConfig.exports` entry points." Entry pointS but every generated `api-extractor.json` pointed at the package's main entry and nothing else, so **87 declared subpaths had no golden at all**: `@voltro/protocol/apikey`, the nine `@voltro/plugin-auth/*`, `@voltro/web/hooks`, `@voltro/database/sql`, and the rest.
121
226
 
122
- **A monorepo may keep ONE root `package.json`** with its apps carrying only an `app.config.ts`. Running `voltro update` inside such an app used to say "no package.json at <dir>"true, and useless. It now recognises the layout, says it is supported, and prints the command with the root already filled in.
123
- - **@voltro/cli** — **`voltro update --only <id>`**, and a summary that stops contradicting itself.
227
+ That is the signal that forces a `BREAKING` changelog entry and its codemod, and on a subpath it was simply absent. `ApiKeyStrategyOptions.resolveKey` gained a parameter in this same release and no gate said anythingadditive, so harmless, but the repo's own "more precise is still breaking" rule (the one that cost an adopter 102 hand-fixes) would have shipped silently through the same hole.
124
228
 
125
- `--only` runs just the named codemods (repeatable or comma-separated; ids are what `--dry-run` prints). The ask behind it: two of three codemods were load-bearing for one app without them, 52 type errors and 51 routes that 404 while the third was elective, and the repairing tool refuses on a dirty tree, so there was no way to take the necessary half first.
229
+ `gen-api-extractor.mjs` already the single source of truth for this wiring now emits one config per entry point (73 → 160) and one golden each, derived from the exports map so the checked set is BY CONSTRUCTION the published set. It also deletes configs and goldens for entry points a package no longer exports: a golden nothing runs reads exactly like covered surface. The per-package `api:check` chains its configs with `&&` rather than a loop, so the first failure stops and reports — a loop is what let the local gate score a green `api-surface` over two stale goldens.
126
230
 
127
- It is deliberately NOT a `--required` flag over a REQUIRED/OPTIONAL axis on each codemod. "Required" would have to mean *this app does not run without it*, and that is a property of the app: `03_pages-suffix` is unavoidable for a project with pages and irrelevant to an api-only one. Marking it on the codemod would encode a guess as a contract. An unknown id is an error listing the ids that ARE available — "it did nothing" and "you typed it wrong" otherwise look identical.
231
+ Verified by breaking a subpath signature on purpose: `api:check` now exits 1 and names `protocol-apikey.api.md`.
128
232
 
129
- **The summary counted only edited files, and renames vanished from it.** The before-snapshot is keyed by PATH, so a moved file has no entry under its new one: a pass that renamed 238 files and edited 9 importers reported `(9 files)`, understating the change by a factor of 26 in the line a user plans around. Moves are now paired by content and reported separately — `(238 renamed, 9 edited)`.
233
+ Regenerating also fixed real drift in the shared path map `@voltro/cli/serveEntry`, `/startEntry` and `/devActivity` ship but were missing from every package's `tsconfig.api-extractor.json`.
234
+ - `pnpm gate` ran each ci.yml `run:` block with `pipefail` but not `-e`, while GitHub Actions' default shell is `bash --noprofile --norc -eo pipefail`. A multi-command block whose MIDDLE command failed carried on, and the step's status became the status of the last command — so the `api-surface` step, a `for` loop over every package's `api:check`, printed two API-drift warnings and still reported `✓`. The gate said green on a tree whose CI job goes red, which is the one thing it exists to prevent.
130
235
 
131
- **A dry run no longer speaks in the past tense.** It printed `✓ <id> <title>`, the same line a real run prints. Now `·` and `[would apply]`.
236
+ Fixed, and it now ships a `--selftest` that runs first (silent unless it finds something), matching the changelog and message-API gates. Its first case is the exact shape that hid this — a failing middle command with a passing last one — and it goes red without the `-e`.
@@ -11,16 +11,16 @@
11
11
  "dependencies": {
12
12
  "@effect/platform": "^0.96.1",
13
13
  "@effect/rpc": "^0.75.1",
14
- "@voltro/ai": "0.16.0",
15
- "@voltro/cli": "0.16.0",
16
- "@voltro/database": "0.16.0",
17
- "@voltro/env": "0.16.0",
18
- "@voltro/protocol": "0.16.0",
19
- "@voltro/runtime": "0.16.0",
14
+ "@voltro/ai": "0.18.0",
15
+ "@voltro/cli": "0.18.0",
16
+ "@voltro/database": "0.18.0",
17
+ "@voltro/env": "0.18.0",
18
+ "@voltro/protocol": "0.18.0",
19
+ "@voltro/runtime": "0.18.0",
20
20
  "effect": "^3.21.2"
21
21
  },
22
22
  "devDependencies": {
23
- "@voltro/testing": "0.16.0",
23
+ "@voltro/testing": "0.18.0",
24
24
  "typescript": "^5.7.0",
25
25
  "vitest": "^3.0.0"
26
26
  }
@@ -12,17 +12,17 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.16.0",
16
- "@voltro/database": "0.16.0",
17
- "@voltro/env": "0.16.0",
18
- "@voltro/plugin-auth": "0.16.0",
19
- "@voltro/protocol": "0.16.0",
20
- "@voltro/runtime": "0.16.0",
21
- "@voltro/sql-postgres": "0.16.0",
15
+ "@voltro/cli": "0.18.0",
16
+ "@voltro/database": "0.18.0",
17
+ "@voltro/env": "0.18.0",
18
+ "@voltro/plugin-auth": "0.18.0",
19
+ "@voltro/protocol": "0.18.0",
20
+ "@voltro/runtime": "0.18.0",
21
+ "@voltro/sql-postgres": "0.18.0",
22
22
  "effect": "^3.21.2"
23
23
  },
24
24
  "devDependencies": {
25
- "@voltro/testing": "0.16.0",
25
+ "@voltro/testing": "0.18.0",
26
26
  "typescript": "^5.7.0",
27
27
  "vitest": "^3.0.0"
28
28
  }
@@ -12,16 +12,16 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.16.0",
16
- "@voltro/database": "0.16.0",
17
- "@voltro/env": "0.16.0",
18
- "@voltro/plugin-multitenancy": "0.16.0",
19
- "@voltro/protocol": "0.16.0",
20
- "@voltro/runtime": "0.16.0",
15
+ "@voltro/cli": "0.18.0",
16
+ "@voltro/database": "0.18.0",
17
+ "@voltro/env": "0.18.0",
18
+ "@voltro/plugin-multitenancy": "0.18.0",
19
+ "@voltro/protocol": "0.18.0",
20
+ "@voltro/runtime": "0.18.0",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.16.0",
24
+ "@voltro/testing": "0.18.0",
25
25
  "typescript": "^5.7.0",
26
26
  "vitest": "^3.0.0"
27
27
  }
@@ -12,16 +12,16 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.16.0",
16
- "@voltro/database": "0.16.0",
17
- "@voltro/env": "0.16.0",
18
- "@voltro/plugin-deactivation": "0.16.0",
19
- "@voltro/protocol": "0.16.0",
20
- "@voltro/runtime": "0.16.0",
15
+ "@voltro/cli": "0.18.0",
16
+ "@voltro/database": "0.18.0",
17
+ "@voltro/env": "0.18.0",
18
+ "@voltro/plugin-deactivation": "0.18.0",
19
+ "@voltro/protocol": "0.18.0",
20
+ "@voltro/runtime": "0.18.0",
21
21
  "effect": "^3.21.2"
22
22
  },
23
23
  "devDependencies": {
24
- "@voltro/testing": "0.16.0",
24
+ "@voltro/testing": "0.18.0",
25
25
  "typescript": "^5.7.0",
26
26
  "vitest": "^3.0.0"
27
27
  }
@@ -12,18 +12,18 @@
12
12
  "dependencies": {
13
13
  "@react-email/components": "^1.0.12",
14
14
  "@react-email/render": "^1.4.0",
15
- "@voltro/cli": "0.16.0",
16
- "@voltro/database": "0.16.0",
17
- "@voltro/env": "0.16.0",
18
- "@voltro/plugin-mail": "0.16.0",
19
- "@voltro/plugin-multitenancy": "0.16.0",
20
- "@voltro/protocol": "0.16.0",
21
- "@voltro/runtime": "0.16.0",
15
+ "@voltro/cli": "0.18.0",
16
+ "@voltro/database": "0.18.0",
17
+ "@voltro/env": "0.18.0",
18
+ "@voltro/plugin-mail": "0.18.0",
19
+ "@voltro/plugin-multitenancy": "0.18.0",
20
+ "@voltro/protocol": "0.18.0",
21
+ "@voltro/runtime": "0.18.0",
22
22
  "effect": "^3.21.2",
23
23
  "react": "^19.0.0"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.16.0",
26
+ "@voltro/testing": "0.18.0",
27
27
  "typescript": "^5.7.0",
28
28
  "vitest": "^3.0.0"
29
29
  }
@@ -12,18 +12,18 @@
12
12
  "dependencies": {
13
13
  "@effect/platform": "^0.96.1",
14
14
  "@effect/rpc": "^0.75.1",
15
- "@voltro/cli": "0.16.0",
16
- "@voltro/database": "0.16.0",
17
- "@voltro/env": "0.16.0",
18
- "@voltro/plugin-multitenancy": "0.16.0",
19
- "@voltro/plugin-storage": "0.16.0",
20
- "@voltro/protocol": "0.16.0",
21
- "@voltro/runtime": "0.16.0",
22
- "@voltro/sql-mysql": "0.16.0",
15
+ "@voltro/cli": "0.18.0",
16
+ "@voltro/database": "0.18.0",
17
+ "@voltro/env": "0.18.0",
18
+ "@voltro/plugin-multitenancy": "0.18.0",
19
+ "@voltro/plugin-storage": "0.18.0",
20
+ "@voltro/protocol": "0.18.0",
21
+ "@voltro/runtime": "0.18.0",
22
+ "@voltro/sql-mysql": "0.18.0",
23
23
  "effect": "^3.21.2"
24
24
  },
25
25
  "devDependencies": {
26
- "@voltro/testing": "0.16.0",
26
+ "@voltro/testing": "0.18.0",
27
27
  "typescript": "^5.7.0",
28
28
  "vitest": "^3.0.0"
29
29
  }