@voltro/cli 0.44.0 → 0.45.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.
- package/CHANGELOG.md +120 -1
- package/dist/{apiBuild-DvWreIQP.js → apiBuild-CDQBNRDx.js} +1 -1
- package/dist/apiBuild-yJCqJYQ8.js +2 -0
- package/dist/bin.d.ts +16 -0
- package/dist/bin.js +46 -11
- package/dist/{build-Ckc-FA7d.js → build-yvsLG77U.js} +7 -7
- package/dist/{checkCommand-BNfEs4KG.js → checkCommand-Boxov8lr.js} +11 -11
- package/dist/{checkCommand-AlbawIal.js → checkCommand-lrzwOzV1.js} +1 -1
- package/dist/codegen-DcE2li1i.js +2 -0
- package/dist/{codegen-CbpGCWLG.js → codegen-sbN1Y8PB.js} +37 -37
- package/dist/codegenCommand-CQOLytL-.js +144 -0
- package/dist/{codemodRunner-BKTnaGOz.js → codemodRunner-5ZhDH3rB.js} +41 -0
- package/dist/{commands-fsnezHLH.js → commands-CEkVoFM5.js} +19 -19
- package/dist/{dashboardCommand-DHDmlo9A.js → dashboardCommand-0ahyp-zA.js} +1 -1
- package/dist/{dataCommand-sfpGV694.js → dataCommand-D4IlZcih.js} +277 -210
- package/dist/{dev-D-JZ6CGQ.js → dev-DAM5CnAa.js} +1856 -1801
- package/dist/{dev-BZO5aLdr.js → dev-P4HyeY9W.js} +1 -1
- package/dist/devActivity.d.ts +16 -0
- package/dist/doctorCommand-2K1dIzYk.js +2 -0
- package/dist/{doctorCommand-BlUyESpD.js → doctorCommand-DW2cwAvC.js} +244 -223
- package/dist/doctorSourceRelations-CPjSwYuZ.js +83 -0
- package/dist/{dormancyCommand-lQxdrU8C.js → dormancyCommand-BqQz7lbM.js} +1 -1
- package/dist/{embeddingsCommand-ByBTju6X.js → embeddingsCommand-DJyArpU4.js} +1 -1
- package/dist/{envCommand-CpCbYAgR.js → envCommand-CviRZx1L.js} +1 -1
- package/dist/{evolveCommand-CBV3-TtK.js → evolveCommand-qdlP-R9G.js} +2 -2
- package/dist/index.d.ts +16 -0
- package/dist/index.js +1 -1
- package/dist/{infoCommand-DJoA9cKD.js → infoCommand-CNWPSUcW.js} +1 -1
- package/dist/inspectState.d.ts +16 -0
- package/dist/{runtimeTrace-DJ9tPhrc.js → runtimeTrace-BDZSt9W_.js} +1 -1
- package/dist/{sdkgen-7dr8-Onw.js → sdkgen-sNaD3oZB.js} +1 -1
- package/dist/{serveCommand-BlnAlNQg.js → serveCommand-BBhT2U94.js} +731 -737
- package/dist/serveCommand-DY2jLy0o.js +2 -0
- package/dist/serveEntry.d.ts +16 -0
- package/dist/serveEntry.js +1 -1
- package/dist/startEntry.d.ts +16 -0
- package/dist/startup.d.ts +16 -0
- package/dist/{test-CLYXrZ2F.js → test-CHb7UwvE.js} +1 -1
- package/dist/updateCommand-BH6EGuCc.js +2 -0
- package/dist/{updateCommand-BvRidHp6.js → updateCommand-rCnhaj84.js} +1 -1
- package/dist/{webhooksCommand-BfO_JF2b.js → webhooksCommand-C0iZZfaI.js} +1 -1
- package/package.json +23 -17
- package/templates/AGENTS.md +1 -1
- package/templates/agent-docs/_index.md +1 -1
- package/templates/agent-docs/cli.md +63 -3
- package/templates/agent-docs/data.md +33 -3
- package/templates/agent-docs/plugins/versioning.md +2 -0
- package/templates/agent-docs/whats-new.md +83 -14
- package/templates/apps/api-ai/package.json +7 -7
- package/templates/apps/api-auth/package.json +8 -8
- package/templates/apps/api-backend/package.json +7 -7
- package/templates/apps/api-backend-deactivation/package.json +7 -7
- package/templates/apps/api-backend-mail/package.json +8 -8
- package/templates/apps/api-backend-mariadb/package.json +9 -9
- package/templates/apps/api-backend-sqlite/package.json +8 -8
- package/templates/apps/api-backend-storage/package.json +8 -8
- package/templates/apps/api-cms/package.json +10 -10
- package/templates/apps/api-collab/package.json +8 -8
- package/templates/apps/api-data-advanced/package.json +8 -8
- package/templates/apps/api-durable/package.json +8 -8
- package/templates/apps/api-feature-flags/package.json +9 -9
- package/templates/apps/api-governance/package.json +8 -8
- package/templates/apps/api-kv/package.json +8 -8
- package/templates/apps/api-moderation/package.json +8 -8
- package/templates/apps/api-observability/package.json +8 -8
- package/templates/apps/api-ratelimit/package.json +8 -8
- package/templates/apps/api-rbac/package.json +8 -8
- package/templates/apps/api-rest/package.json +7 -7
- package/templates/apps/api-saas/package.json +11 -11
- package/templates/apps/api-saas-starter/package.json +10 -10
- package/templates/apps/api-search/package.json +8 -8
- package/templates/apps/api-status/package.json +8 -8
- package/templates/apps/api-versioning/package.json +8 -8
- package/templates/apps/api-webhooks/package.json +9 -9
- package/templates/apps/changelog/package.json +6 -6
- package/templates/apps/edge-functions/package.json +2 -2
- package/templates/apps/frontend-admin/package.json +8 -8
- package/templates/apps/frontend-app/package.json +9 -9
- package/templates/apps/frontend-auth/package.json +8 -8
- package/templates/apps/frontend-blank/package.json +7 -7
- package/templates/apps/frontend-cms/package.json +9 -9
- package/templates/apps/frontend-collab/package.json +10 -10
- package/templates/apps/frontend-contact/package.json +7 -7
- package/templates/apps/frontend-dashboard/package.json +7 -7
- package/templates/apps/frontend-docs/package.json +7 -7
- package/templates/apps/frontend-i18n/package.json +6 -6
- package/templates/apps/frontend-landing/package.json +7 -7
- package/templates/apps/frontend-portal/package.json +8 -8
- package/templates/apps/frontend-saas/package.json +8 -8
- package/templates/apps/frontend-spa/package.json +7 -7
- package/templates/apps/frontend-ssr/package.json +7 -7
- package/templates/apps/frontend-ssr-api/package.json +8 -8
- package/templates/apps/frontend-static-blog/package.json +6 -6
- package/templates/apps/frontend-status/package.json +8 -8
- package/templates/apps/mobile-app/package.json +4 -4
- package/dist/apiBuild-XaW40W8x.js +0 -2
- package/dist/codegen-i8QGcHsi.js +0 -2
- package/dist/codegenCommand-iClXoRCW.js +0 -129
- package/dist/doctorCommand-BeLOwq7S.js +0 -2
- package/dist/serveCommand-Dg_E1ev4.js +0 -2
- package/dist/updateCommand-DcKVmLVN.js +0 -2
package/dist/serveEntry.d.ts
CHANGED
|
@@ -20,3 +20,19 @@ export declare const registerDriver: (spec: string, mod: Record<string, unknown>
|
|
|
20
20
|
export declare const runServe: (args: ReadonlyArray<string>) => Promise<number>;
|
|
21
21
|
|
|
22
22
|
export { }
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
declare module '@voltro/protocol' {
|
|
26
|
+
interface VoltroTableNames {
|
|
27
|
+
'tasks': true;
|
|
28
|
+
'task_sub_tasks': true;
|
|
29
|
+
'_voltro_workflow_runs': true;
|
|
30
|
+
'_voltro_workflow_run_steps': true;
|
|
31
|
+
'_voltro_workflow_run_events': true;
|
|
32
|
+
'_voltro_workflow_events': true;
|
|
33
|
+
'_voltro_workflow_event_deliveries': true;
|
|
34
|
+
'_voltro_approvals': true;
|
|
35
|
+
'_voltro_connections': true;
|
|
36
|
+
'_voltro_undo_log': true;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/dist/serveEntry.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as e } from "./loadEnv-D9nEOClM.js";
|
|
2
2
|
import { r as t } from "./appModuleLoader-C9r9mxZt.js";
|
|
3
3
|
import { i as n } from "./dialectDriver-czCHYpeH.js";
|
|
4
|
-
import { t as r } from "./serveCommand-
|
|
4
|
+
import { t as r } from "./serveCommand-BBhT2U94.js";
|
|
5
5
|
export { e as loadDotEnv, t as registerAppModules, n as registerDriver, r as runServe };
|
package/dist/startEntry.d.ts
CHANGED
|
@@ -3,3 +3,19 @@ export declare const loadDotEnv: (argv: ReadonlyArray<string>) => void;
|
|
|
3
3
|
export declare const runStartCommand: (args: ReadonlyArray<string>) => Promise<number>;
|
|
4
4
|
|
|
5
5
|
export { }
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
declare module '@voltro/protocol' {
|
|
9
|
+
interface VoltroTableNames {
|
|
10
|
+
'tasks': true;
|
|
11
|
+
'task_sub_tasks': true;
|
|
12
|
+
'_voltro_workflow_runs': true;
|
|
13
|
+
'_voltro_workflow_run_steps': true;
|
|
14
|
+
'_voltro_workflow_run_events': true;
|
|
15
|
+
'_voltro_workflow_events': true;
|
|
16
|
+
'_voltro_workflow_event_deliveries': true;
|
|
17
|
+
'_voltro_approvals': true;
|
|
18
|
+
'_voltro_connections': true;
|
|
19
|
+
'_voltro_undo_log': true;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/startup.d.ts
CHANGED
|
@@ -63,3 +63,19 @@ export declare interface StartupHandle {
|
|
|
63
63
|
export declare const startupTimeoutMsFromEnv: (envValue: string | undefined) => number;
|
|
64
64
|
|
|
65
65
|
export { }
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
declare module '@voltro/protocol' {
|
|
69
|
+
interface VoltroTableNames {
|
|
70
|
+
'tasks': true;
|
|
71
|
+
'task_sub_tasks': true;
|
|
72
|
+
'_voltro_workflow_runs': true;
|
|
73
|
+
'_voltro_workflow_run_steps': true;
|
|
74
|
+
'_voltro_workflow_run_events': true;
|
|
75
|
+
'_voltro_workflow_events': true;
|
|
76
|
+
'_voltro_workflow_event_deliveries': true;
|
|
77
|
+
'_voltro_approvals': true;
|
|
78
|
+
'_voltro_connections': true;
|
|
79
|
+
'_voltro_undo_log': true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -79,7 +79,7 @@ var d = l({ scope: "voltro:test" }), f = (e) => {
|
|
|
79
79
|
}, C = async (e) => {
|
|
80
80
|
let t = m(e);
|
|
81
81
|
if (!o(t.root)) return console.error(`voltro test: no such directory: ${t.root}`), 1;
|
|
82
|
-
let { checkRpcGroupFreshness: n } = await import("./codegen-
|
|
82
|
+
let { checkRpcGroupFreshness: n } = await import("./codegen-DcE2li1i.js"), r = await n(t.root);
|
|
83
83
|
if (r.kind === "stale") return console.error(`voltro test: ${r.file} is stale — ${r.reason}.`), console.error(" The tests would run against the previously generated procedure group, pass, and prove nothing about the descriptors you just edited.\n Run `voltro codegen` (or boot `voltro dev` once) and try again."), 1;
|
|
84
84
|
d.info("running vitest", {
|
|
85
85
|
root: t.root,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as e, r as t, t as n } from "./codemodRunner-
|
|
1
|
+
import { n as e, r as t, t as n } from "./codemodRunner-5ZhDH3rB.js";
|
|
2
2
|
import { basename as r, dirname as i, join as a, relative as o, resolve as s } from "node:path";
|
|
3
3
|
import { existsSync as c, readFileSync as l, readdirSync as u, statSync as d, writeFileSync as f } from "node:fs";
|
|
4
4
|
import { totalmem as p } from "node:os";
|
|
@@ -222,7 +222,7 @@ createVerifier({ secret: [process.env.WEBHOOK_SECRET, process.env.WEBHOOK_SECRET
|
|
|
222
222
|
...t === void 0 ? {} : { payload: t }
|
|
223
223
|
};
|
|
224
224
|
}, S = u({ scope: "voltro:webhooks" }), C = ["--out", "--name"], w = async (e) => {
|
|
225
|
-
let { walk: t, loadDiscovered: n } = await import("./dev-
|
|
225
|
+
let { walk: t, loadDiscovered: n } = await import("./dev-P4HyeY9W.js"), { outgoingFromEvents: r } = await import("./webhookDiscovery-il9ti-HE.js");
|
|
226
226
|
return r((await n(await t(e))).events.map((e) => ({
|
|
227
227
|
file: e.file,
|
|
228
228
|
descriptor: e.descriptor
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voltro/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.45.0",
|
|
4
4
|
"description": "The `voltro` CLI — dev server, codegen, migrations, project scaffolding, agent-docs seeding, and production serve.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"voltro",
|
|
@@ -640,6 +640,12 @@
|
|
|
640
640
|
"title": "`runImport` returns an ImportOutcome — read `.manifest` for the bundle",
|
|
641
641
|
"kind": "manual"
|
|
642
642
|
},
|
|
643
|
+
{
|
|
644
|
+
"version": "0.45.0",
|
|
645
|
+
"id": "0.45.0/01_typed-source-table-name",
|
|
646
|
+
"title": "`source:` is typechecked against the app’s tables, from the next `voltro dev`",
|
|
647
|
+
"kind": "manual"
|
|
648
|
+
},
|
|
643
649
|
{
|
|
644
650
|
"version": "0.5.0",
|
|
645
651
|
"id": "0.5.0/01_one-terminal-exactly-one",
|
|
@@ -727,22 +733,22 @@
|
|
|
727
733
|
"@effect/platform-node": "^0.108.0",
|
|
728
734
|
"@effect/sql": "^0.52.0",
|
|
729
735
|
"@effect/workflow": "^0.19.0",
|
|
730
|
-
"@voltro/ai": "0.
|
|
731
|
-
"@voltro/cache": "0.
|
|
732
|
-
"@voltro/data-transfer": "0.
|
|
733
|
-
"@voltro/database": "0.
|
|
734
|
-
"@voltro/env": "0.
|
|
735
|
-
"@voltro/kv": "0.
|
|
736
|
-
"@voltro/logger": "0.
|
|
737
|
-
"@voltro/plugin-auth": "0.
|
|
738
|
-
"@voltro/plugin-broadcast": "0.
|
|
739
|
-
"@voltro/plugin-mail": "0.
|
|
740
|
-
"@voltro/plugin-storage": "0.
|
|
741
|
-
"@voltro/plugin-webhooks": "0.
|
|
742
|
-
"@voltro/protocol": "0.
|
|
743
|
-
"@voltro/runtime": "0.
|
|
744
|
-
"@voltro/serverless": "0.
|
|
745
|
-
"@voltro/workflow": "0.
|
|
736
|
+
"@voltro/ai": "0.45.0",
|
|
737
|
+
"@voltro/cache": "0.45.0",
|
|
738
|
+
"@voltro/data-transfer": "0.45.0",
|
|
739
|
+
"@voltro/database": "0.45.0",
|
|
740
|
+
"@voltro/env": "0.45.0",
|
|
741
|
+
"@voltro/kv": "0.45.0",
|
|
742
|
+
"@voltro/logger": "0.45.0",
|
|
743
|
+
"@voltro/plugin-auth": "0.45.0",
|
|
744
|
+
"@voltro/plugin-broadcast": "0.45.0",
|
|
745
|
+
"@voltro/plugin-mail": "0.45.0",
|
|
746
|
+
"@voltro/plugin-storage": "0.45.0",
|
|
747
|
+
"@voltro/plugin-webhooks": "0.45.0",
|
|
748
|
+
"@voltro/protocol": "0.45.0",
|
|
749
|
+
"@voltro/runtime": "0.45.0",
|
|
750
|
+
"@voltro/serverless": "0.45.0",
|
|
751
|
+
"@voltro/workflow": "0.45.0",
|
|
746
752
|
"chokidar": "^5.0.0",
|
|
747
753
|
"ioredis": "^5.11.1",
|
|
748
754
|
"tinyglobby": "^0.2.17",
|
package/templates/AGENTS.md
CHANGED
|
@@ -707,7 +707,7 @@ each plugin's own README.
|
|
|
707
707
|
|
|
708
708
|
| Topic | Open | Summary |
|
|
709
709
|
|---|---|---|
|
|
710
|
-
| **What's new in 0.
|
|
710
|
+
| **What's new in 0.45.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
711
711
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
712
712
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
713
713
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -9,7 +9,7 @@ each plugin's own README.
|
|
|
9
9
|
|
|
10
10
|
| Topic | Open | Summary |
|
|
11
11
|
|---|---|---|
|
|
12
|
-
| **What's new in 0.
|
|
12
|
+
| **What's new in 0.45.0** | `node_modules/@voltro/cli/templates/agent-docs/whats-new.md` | Everything that changed in this version. Read it before hand-rolling something the framework may now ship. |
|
|
13
13
|
| AI | `node_modules/@voltro/cli/templates/agent-docs/ai.md` | How Voltro treats AI — agents, tools, streaming, RAG — all primitives over the same WebSocket as the rest of the framework. |
|
|
14
14
|
| Authentication | `node_modules/@voltro/cli/templates/agent-docs/authentication.md` | How @voltro/plugin-auth wires password + session-cookie auth across api + web, plus the pluggable identity-strategy protocol. |
|
|
15
15
|
| Caching | `node_modules/@voltro/cli/templates/agent-docs/caching.md` | Voltro's caching layer (@voltro/cache) — an always-on memory default, swappable Redis-compatible backends, a low-level wrap primitive, and automatic query-result invalidation. |
|
|
@@ -1377,6 +1377,22 @@ Docker step) and prints the exact remedy: add a `voltro build .` step before
|
|
|
1377
1377
|
`voltro serve .`. Drop it into your image build right after `voltro build` to
|
|
1378
1378
|
guarantee the artefact is present before the image ships.
|
|
1379
1379
|
|
|
1380
|
+
### Relations a query loads but does not declare
|
|
1381
|
+
|
|
1382
|
+
An eager-loaded relation is part of the RESULT, so its table has to be in `source:` or the view stops updating when it changes:
|
|
1383
|
+
|
|
1384
|
+
```
|
|
1385
|
+
✗ 1 query loads a relation it does not declare:
|
|
1386
|
+
tasks.getById: eager-loads `subTasks` from 'tasks' but does not declare
|
|
1387
|
+
'task_sub_tasks' in `source:` — the view will not update when 'task_sub_tasks' changes.
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
This is the failure that looks like a broken feature and is not: the write lands, a reload shows it, and every test of the write path is green. The name in `source:` is spelled right and the table exists, so neither the typed `source:` nor the boot audit has anything to say.
|
|
1391
|
+
|
|
1392
|
+
The rule has no exception list, deliberately. It resolves `.with({ … })` keys through the relation registry, so the missing table is a fact rather than an inference — and a **many-to-many** is reported twice when needed, because the junction table is where a link add/remove actually writes.
|
|
1393
|
+
|
|
1394
|
+
It reads what it can read literally: a computed `.with()` key yields nothing rather than a guess.
|
|
1395
|
+
|
|
1380
1396
|
### The access-decision gate
|
|
1381
1397
|
|
|
1382
1398
|
Before the authz scan below — which is a heuristic over executor SOURCE — doctor
|
|
@@ -2607,7 +2623,7 @@ Tenant-scope details:
|
|
|
2607
2623
|
|
|
2608
2624
|
- **A tenant scope without a tenant id refuses loudly** (`ScopeError`): pass `--tenant <id>` (CLI) or `scope.tenantId` (API/profile). It never falls back to an unfiltered export.
|
|
2609
2625
|
- **Which tables count as tenant-scoped** comes from the `tenant()` mixin metadata when the CLI / admin endpoint can read the declared schema (authoritative — a table can carry a `tenantId` column *without* being tenant-scoped, e.g. a global `users` table's active-org pointer). Without that metadata the exporter falls back to a documented heuristic: any table with a `tenantId` column.
|
|
2610
|
-
- A **cross-tenant reference** (a tenant-A row pointing at a tenant-B row) is never followed — the bundle stays tenant-clean and the reference dangles; importing such a bundle reports it loudly (`
|
|
2626
|
+
- A **cross-tenant reference** (a tenant-A row pointing at a tenant-B row) is never followed — the bundle stays tenant-clean and the reference dangles; importing such a bundle reports it loudly (`RowsRefusedError`) unless the target already has the row.
|
|
2611
2627
|
|
|
2612
2628
|
### Consistency: `live` vs `snapshot`
|
|
2613
2629
|
|
|
@@ -2745,7 +2761,41 @@ A row whose write fails on a foreign-key constraint — a forward reference from
|
|
|
2745
2761
|
2. **FK-shedding** — rows still stuck are written with their FK-bearing columns set to `NULL` (possible wherever those columns are nullable), which breaks row cycles on every dialect without session-level constraint toggles.
|
|
2746
2762
|
3. **Patch pass** — shed rows are re-written with the full bundle row, restoring the FK values.
|
|
2747
2763
|
|
|
2748
|
-
Anything still unresolvable — the parent row exists in **neither the bundle nor the target**,
|
|
2764
|
+
Anything still unresolvable — the parent row exists in **neither the bundle nor the target**, a `NOT NULL` FK cycle, or a row the target refuses for a reason of its own — fails with a typed `RowsRefusedError`. Held rows are the exception set, not the data set: memory is bounded by how many rows dangle at load time.
|
|
2765
|
+
|
|
2766
|
+
#### Reading a refusal
|
|
2767
|
+
|
|
2768
|
+
The error carries three things, and they answer different questions:
|
|
2769
|
+
|
|
2770
|
+
| Field | What it is |
|
|
2771
|
+
|---|---|
|
|
2772
|
+
| `totalCount` / `primaryCount` | how many rows were refused, and how many of those are the actual failures. A row is **derived** when one of its reference columns points at another row that also failed — it could not have landed whatever it contained, so it says nothing about itself. In an FK-dense bundle these dominate. |
|
|
2773
|
+
| `byTable` | **complete** per-table counts (`refused`, `primary`), worst first. |
|
|
2774
|
+
| `rows` | up to 20 refused rows — primary ones first — each with `table`, `id`, `reason`. |
|
|
2775
|
+
|
|
2776
|
+
`rows` is **capped and `byTable` is not**, and that distinction is worth one sentence: tallying the tables in the printed list answers "how big is the cap", not "which tables failed". The CLI prints the `byTable` line above the list and says so when the list is truncated:
|
|
2777
|
+
|
|
2778
|
+
```
|
|
2779
|
+
import refused 35 row(s), of which 35 are the actual failures — the rest could not land because a row they reference did not.
|
|
2780
|
+
by table: vacations 26, weeklyUpdates 9
|
|
2781
|
+
vacations v1 not-null constraint userId: the column requires a value [ER_BAD_NULL_ERROR/1048]
|
|
2782
|
+
…
|
|
2783
|
+
(the list above is capped at 20 of 35 — the counts by table are complete)
|
|
2784
|
+
```
|
|
2785
|
+
|
|
2786
|
+
A refused import ends with a **non-zero exit status and that report** — not with a stack trace. A refusal is a condition with a named cause, not a framework defect, so it is not dressed as one. The report is the same on both transports: over `--target api` the refusal crosses as a 500 whose message embeds it, and the CLI renders that rather than printing the body raw.
|
|
2787
|
+
|
|
2788
|
+
**`RowsRefusedError` keeps its tag in every mode, `--atomic` included.** That is worth stating because it is the mode `replace` uses by default: the whole table phase runs in one transaction, and rolling that transaction back needs a rejection — but the rejection carries the typed error, not a rendering of it. So `Effect.catchTag('RowsRefusedError', …)` works on the default path, which is the one most likely to raise one.
|
|
2789
|
+
|
|
2790
|
+
One trap if you consume `runImport` yourself: what `Effect.runPromise` **rejects** with is a `FiberFailure`, and `err._tag` on one of those reads `undefined` however good the error inside is. Use `Effect.catchTag` on the effect, or `asImportError(err)` (exported from `@voltro/data-transfer`) on the rejection — reading the tag off the caught value takes the "not my error" branch every time.
|
|
2791
|
+
|
|
2792
|
+
A row's `reason` is stated as precisely as the driver allows, in three tiers:
|
|
2793
|
+
|
|
2794
|
+
1. **the rule, in your schema's vocabulary** — `unique constraint PRIMARY: a row with this value already exists [ER_DUP_ENTRY/1062]`, `foreign key teams_ibfk_1: the referenced row does not exist (import it first, or check the bundle's table order) [ER_NO_REFERENCED_ROW_2/1452]`. The driver's own code is appended, because that is what you grep a log for;
|
|
2795
|
+
2. **the driver's own message + code**, for anything it refused that is not one of the five integrity rules — `Data too long for column 'v' at row 1 [ER_DATA_TOO_LONG/1406]`;
|
|
2796
|
+
3. **that there was nothing**, when no driver detail is reachable at all: `the database refused the write and the driver gave no detail` — plus `(via …)`, the chain of wrappers, whenever that chain has more than one layer to name. Read this tier as "not a constraint": a guard, a row filter, or a failure whose words did not survive. When it fires, the run also **logs the full rendering** of the first three such rows (table, primary key, and the whole error as it rendered) and counts the rest. That log line is never returned over the wire — it carries our stack frames, and on some engines a driver's sentence carries row data — so on `--target api` you read it in the instance's log.
|
|
2797
|
+
|
|
2798
|
+
Unlike the rpc wire, this string DOES include the driver's own words. The audience is the difference: it is read only by whoever ran `voltro data import` — the holder of the data-transfer secret, who supplied the rows and can export the whole target anyway.
|
|
2749
2799
|
|
|
2750
2800
|
### Write modes (`--mode`)
|
|
2751
2801
|
|
|
@@ -2878,6 +2928,16 @@ voltro data import ./out --target api --api-url https://api.example --token $SEC
|
|
|
2878
2928
|
|
|
2879
2929
|
Note the asymmetry when a deadline *is* hit: re-running an `upsert` after the first run has finished is safe (it is idempotent); re-running a **`replace`** while the first is still mid-flight would empty the target under it. Check the instance's log before deciding — the message says so too.
|
|
2880
2930
|
|
|
2931
|
+
An instance that is **not answering at all** is a different message, and it names the instance rather than a database:
|
|
2932
|
+
|
|
2933
|
+
```
|
|
2934
|
+
voltro: api import: the instance at api.example.com is not answering (ETIMEDOUT).
|
|
2935
|
+
That address came from --api-url (or VOLTRO_API_URL) — it is the running instance, not the database.
|
|
2936
|
+
Nothing was sent, so nothing was imported. …
|
|
2937
|
+
```
|
|
2938
|
+
|
|
2939
|
+
Worth one line because the alternative was measured: a connect failure carries an address, a port and an errno, which is the same shape a database driver's carries — so without this the api host was reported as an unreachable *database*, attributed to `DB_URL`, a variable not in play on that run. Every word after the address was wrong, and the address being right is what made it convincing.
|
|
2940
|
+
|
|
2881
2941
|
**Import** (`POST /_voltro/admin/import`) applies a bundle **in the instance's own process** through its store — full app pipeline (validation, mixins, **field encryption**, hooks) AND automatic reactivity. Two transports, neither needs a server-readable path:
|
|
2882
2942
|
- **upload** (default) — the CLI **packs the bundle and uploads the bytes**; the server unpacks to its own temp dir, imports in-process, cleans up. Buffered → small/moderate bundles.
|
|
2883
2943
|
- **storage-pull** (`--bundle-key <key>`) — the CLI sends only `{ "bundleKey": "<key>" }`; the instance **streams that archive from its configured object storage** (S3 / Azure / MinIO / filesystem, resolved from the storage env — `STORAGE_PROVIDER`, `S3_*`, `AZURE_*`, …), no body buffering → arbitrarily large bundles.
|
|
@@ -2977,7 +3037,7 @@ yield* runImport({
|
|
|
2977
3037
|
|
|
2978
3038
|
Every pipeline failure is a **tagged error**, caught by tag with `Effect.catchTag(...)`. The errors that appear on the `runExport` / `runImport` error channels — the **wire errors**, and therefore exactly what the `--target api` admin endpoints surface — are `Schema.TaggedError`, so a handler can declare them on an rpc procedure's `error:` schema and the rpc encoder marshals them across the wire round-trip-safely (no hand-rolled JSON per tag):
|
|
2979
3039
|
|
|
2980
|
-
`BundleError`, `CodecError`, `IntegrityError`, `CrossDialectError`, `ImportModeError`, `
|
|
3040
|
+
`BundleError`, `CodecError`, `IntegrityError`, `CrossDialectError`, `ImportModeError`, `RowsRefusedError`, `SchemaDriftError`, `MaskingError`, `ScopeError`.
|
|
2981
3041
|
|
|
2982
3042
|
The two internal errors — `NativeToolError` (native `backup`/`restore`) and `CompressionError` (folded into `BundleError` by the pipelines) — never cross the wire, so they stay plain `Data.TaggedError`: still catchable by tag, just no Schema surface.
|
|
2983
3043
|
|
|
@@ -1638,14 +1638,44 @@ warning](#fan-out--how-many-subscribers-may-one-change-wake) is the only signal
|
|
|
1638
1638
|
for a subscription that has gone permanently quiet, and an exemption for a name
|
|
1639
1639
|
you invented disables it for the one case it was built for.
|
|
1640
1640
|
|
|
1641
|
+
### A table `source:` is typechecked
|
|
1642
|
+
|
|
1643
|
+
`voltro dev` writes `voltro-tables.generated.d.ts` beside your generated rpc group, listing every table the app has — your entities, your plugins' `extendSchema.tables`, and the framework's own. `source:` is narrowed to those names, so a typo or a table you renamed away is a **compile error**:
|
|
1644
|
+
|
|
1645
|
+
```ts
|
|
1646
|
+
source: 'task_subtasks', // ✗ Type '"task_subtasks"' is not assignable to type 'TableName'
|
|
1647
|
+
source: 'task_sub_tasks', // ✓
|
|
1648
|
+
```
|
|
1649
|
+
|
|
1650
|
+
That matters because the failure it replaces is silent. A `source:` is matched by NAME against change events, so one that matches nothing does not break the query — it makes it never update. The write still lands, a reload still shows it, and the panel keeps showing the old value.
|
|
1651
|
+
|
|
1652
|
+
The file is generated, so **commit it** like the rpc group and let `voltro dev` rewrite it. Before the first run — and in a project that never generates it — `source:` is plain `string` again, which is exactly the previous behaviour; there is no configuration and nothing to opt into.
|
|
1653
|
+
|
|
1654
|
+
Two things it deliberately does not narrow. A **plugin's** route `source:` stays `string`: a plugin ships against many apps and cannot know any of their tables. And nothing that READS a descriptor's source at runtime narrows either — a reader that refused an unknown name would reject the stale name it exists to report.
|
|
1655
|
+
|
|
1656
|
+
**It does not check the other direction — except for relations.** A `source:` that omits a table the query genuinely reads is silent: the name is right, the table exists, and nothing has an opinion. That is the failure that costs a user report — they type, the row lands, and the panel does not move.
|
|
1657
|
+
|
|
1658
|
+
`voltro doctor` closes the part of it that can be closed without guessing:
|
|
1659
|
+
|
|
1660
|
+
```
|
|
1661
|
+
✗ 1 query loads a relation it does not declare:
|
|
1662
|
+
tasks.getById: eager-loads `subTasks` from 'tasks' but does not declare
|
|
1663
|
+
'task_sub_tasks' in `source:` — the view will not update when 'task_sub_tasks' changes.
|
|
1664
|
+
```
|
|
1665
|
+
|
|
1666
|
+
An eager-loaded relation is composition by definition — its rows are IN the result — and its table comes from the relation registry, so the rule has no heuristic and no exception list. A **many-to-many** wants the junction table too, and says so separately: adding or removing a link writes only the junction row, so declaring the target alone leaves the list stale on exactly the operation a user performs to change it.
|
|
1667
|
+
|
|
1668
|
+
What doctor deliberately does NOT do is derive every table an executor reads. That needs a judgement — does this read **compose** the result or merely **restrict** it? — and only composing reads belong in `source:`; a restricting one re-running on every unrelated write puts a hundred lists back on the wire. A scan has to infer that from syntax, and a rule that guesses on a correct codebase teaches you to ignore it.
|
|
1669
|
+
|
|
1670
|
+
So: when a live view does not update, check the writing table is in the reading query's `source:` before anything else.
|
|
1671
|
+
|
|
1641
1672
|
### Pass the channel, not its key
|
|
1642
1673
|
|
|
1643
1674
|
A channel's routing key is `channel:<name>`, and you can read it off
|
|
1644
1675
|
`jobQueue.key`. **Do not write that string into `source:`.** Passing the object
|
|
1645
1676
|
creates an import edge from the query to the declaration, which removes the
|
|
1646
|
-
entire class of stale-source bugs for channels: a
|
|
1647
|
-
|
|
1648
|
-
that is not imported does not exist to be named. The boot audit reports a
|
|
1677
|
+
entire class of stale-source bugs for channels: a channel that is not imported
|
|
1678
|
+
does not exist to be named. The boot audit reports a
|
|
1649
1679
|
`channel:` key nothing declared, for the two ways round it.
|
|
1650
1680
|
|
|
1651
1681
|
Names are lowercase kebab segments separated by dots — `presence`, `job-queue`,
|
|
@@ -27,6 +27,8 @@ export default {
|
|
|
27
27
|
|
|
28
28
|
Every committed change to a listed table appends a row to `_voltro_row_history` (`tableName`, `rowId`, monotonic `version`, `op`, the full `data` snapshot, `changedBy`, `changedAt`, `traceId`, `subjectId`, `procedure`).
|
|
29
29
|
|
|
30
|
+
The history row's own `id` is **derived** from `(tableName, rowId, version)` and has a fixed width — it is a surrogate, and every part of it is already a column beside it, so do not parse or construct it. That width is the point: an `id()` column is `VARCHAR(64)` on mysql/mariadb and `NVARCHAR(64)` on mssql, so a key built by concatenating those parts grew with your **table name** and stopped fitting past 22 characters — which failed every write to that table, not merely an import.
|
|
31
|
+
|
|
30
32
|
|
|
31
33
|
### What gets versioned — opt OUT, not in
|
|
32
34
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# What's new in 0.
|
|
1
|
+
# What's new in 0.45.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,30 +9,99 @@ BREAKING entries name a codemod; run `voltro update` to apply it.
|
|
|
9
9
|
|
|
10
10
|
### ⚠ BREAKING
|
|
11
11
|
|
|
12
|
-
- **@voltro/
|
|
12
|
+
- **@voltro/protocol, @voltro/cli, @voltro/voltro** — `source:` on a query is now typed against the app's own tables, so a typo or a missed rename is a compile error instead of a subscription that goes quiet.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
A `source:` is matched by NAME against change events, so a name matching nothing does not break the query — it makes it permanently silent: it compiles, boots, serves its first snapshot and never updates. From the outside that reads as a feature that does nothing, with a correct write path and green tests behind it. The boot has warned about this since 0.26.0, on both paths; a warning is read once, and a rename lands in a diff where nobody is checking strings.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
`voltro dev` writes `voltro-tables.generated.d.ts` beside the generated rpc group, augmenting `VoltroTableNames` with the FULL live set — app entities, plugin `extendSchema.tables` and the framework's own — from the same binding the boot audit resolves against, so the type and the warning cannot disagree about which tables exist. `source:` narrows to those names.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Nothing changes at runtime: these are still string literals, so a descriptor carrying them is as browser-loadable as before. That is what ruled out accepting the table VALUE — a descriptor is loaded value-level by the web client, and a table value drags `@voltro/database` across that boundary.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
**Breaking, and filed that way after being written up as additive.** The test is not whether a symbol disappeared, it is whether code that compiled can stop: `['tasks', 'agent_messages']` was assignable and is not, which is the whole point where the name is stale and an obstacle where the source is genuinely computed. `normalizeSource`'s parameter narrowed with it. The wide shape stays public as `ReactivitySourceValue` for the computed case.
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
The break does NOT land at upgrade time, which is why the codemod is a written note rather than a transform: right after `voltro update` the generated file does not exist, `keyof VoltroTableNames` is `never`, `TableName` falls back to `string`, and everything compiles as before. The narrowing switches on at the next `voltro dev` — a different command, by which point the change that caused it is no longer what the reader is looking at. A transform could not have found the sites either, since the type that rejects them has not been generated yet. And the two things `tsc` flags — a stale name versus a runtime-computed one — want opposite fixes, so the mechanical one (widen the annotation) would convert every defect this surfaces back into the quiet subscription it exists to expose.
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
Delete the generated file and `source:` widens back to `string`.
|
|
25
|
+
|
|
26
|
+
One deliberate asymmetry, stated because it is one: runtime READERS of a descriptor's source stay wide (`ReactivitySourceValue`). Narrow where an author writes, stay wide where the framework reads — a reader that refused an unknown name would be asserting a fact it cannot check, and the first thing it would reject is the stale name it exists to report.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **@voltro/cli** — `voltro doctor` reports a query that eager-loads a relation and does not declare its table in `source:` — the failure that looks like a broken feature and is not.
|
|
31
|
+
|
|
32
|
+
The write lands, a reload shows it, every test of the write path is green, the name in `source:` is spelled right and the table exists. So neither the typed `source:` nor the boot audit has anything to say, and the only observer is a user watching a panel that does not move.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
✗ 1 query loads a relation it does not declare:
|
|
36
|
+
tasks.getById: eager-loads `subTasks` from 'tasks' but does not declare
|
|
37
|
+
'task_sub_tasks' in `source:` — the view will not update when 'task_sub_tasks' changes.
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
No exception list, deliberately. An eager-loaded relation is composition by definition — its rows are IN the result — and its table comes from the relation registry, so the missing name is a fact rather than an inference. A many-to-many is reported twice when needed: adding or removing a link writes only the JUNCTION row, so declaring the target alone leaves the list stale on exactly the operation a user performs to change it. A computed `.with()` key yields nothing rather than a guess.
|
|
41
|
+
|
|
42
|
+
The general question — every table an executor reads — is NOT answered, on purpose: it needs a compose-versus-restrict judgement a scan can only infer from syntax, and a rule that guesses on a correct codebase teaches its reader to ignore it. Design in `plans/open/framework/source-completeness.md`.
|
|
43
|
+
|
|
44
|
+
Two things around it:
|
|
45
|
+
|
|
46
|
+
- **The stale-`source:` audit covered queries only, on both boot paths.** A stream carries a `source:` too, and a stale one there is the same permanently quiet subscription with a longer-lived connection behind it. Both paths now take the set from one `auditableSources`. - **`voltro codegen` writes the typed-`source:` declaration too**, from the same shared `declaredTableNames` merge the boots use. Letting it lag was the bad direction: a table added since the last `voltro dev` would make a CORRECT `source:` a type error. Both commands now say what they wrote — the narrowing has a silent no-op if the app's tsconfig does not pick the file up, so the write has to be loud enough that a reader can check.
|
|
25
47
|
|
|
26
48
|
### Fixed
|
|
27
49
|
|
|
28
|
-
- **@voltro/
|
|
50
|
+
- **@voltro/database, @voltro/sql-mysql** — A binding failure names the TYPE of every value, so the culprit is read rather than guessed.
|
|
51
|
+
|
|
52
|
+
`ER_WRONG_ARGUMENTS` / 1210 reads like a count problem and often is not: a statement with twelve columns and twelve placeholders is internally consistent, and the driver is refusing one VALUE it cannot bind. Measured against a live mariadb 11.8 and mysql 8.4 (mysql2 3.22), binding to a PREPARED statement:
|
|
53
|
+
|
|
54
|
+
| value | mariadb | mysql | |---|---|---| | plain object | **1210** | accepted | | array | **1210** | accepted | | bigint | accepted | accepted | | Invalid Date | accepted | 1292 |
|
|
55
|
+
|
|
56
|
+
So the same row binds on one engine of the family and not the other — which is how a suite comes to fail on mariadb and pass on mysql in the SAME run, and why the type of each binding is the diagnosis rather than a detail.
|
|
57
|
+
|
|
58
|
+
`describeDriverError` now reports `bindings: id:string data:Object changedAt:Date …` beside the placeholder count and the statement. Types only; a value there would be row data in a log line, the same reason the statement is carried only in its placeholder form.
|
|
59
|
+
|
|
60
|
+
The types travel as a FIELD, not in a message. That is load-bearing: a failing write recorder rethrows with its own sentence, so anything said only in text is dropped exactly where it is needed. `extractDbCause` collects it like any driver field, so it survives every wrapper between the failing statement and the log.
|
|
61
|
+
- **@voltro/database, @voltro/testing** — A driver error now carries the two numbers a binding failure is made of.
|
|
62
|
+
|
|
63
|
+
`ER_WRONG_ARGUMENTS` / errno 1210 means the parameter count did not match the placeholder count — reproduced against mariadb 11.8 by sending one parameter for two `?` — and the message says only `Incorrect arguments to mysqld_stmt_execute`. Neither number was reachable from the error, so an investigation into one of these starts by eliminating hypotheses instead of subtracting.
|
|
64
|
+
|
|
65
|
+
`describeDriverError` reports `placeholders=N` and the statement, and the statement is carried ONLY in its placeholder form. That restriction is measured, not cautious: against mysql2 3.22 the prepared path (`execute`) leaves `?` in `err.sql` because the server did the binding, while the text path (`query`) interpolates and the same field then holds row DATA. The placeholder is the discriminator, and the form that keeps it is exactly the form 1210 arises in.
|
|
66
|
+
|
|
67
|
+
Alongside it, `reportEngineVersion` (`@voltro/testing`): a dialect suite prints the engine BUILD it ran against. A suite that is green on a developer machine and red in CI is only comparable if both name their software, and the test compose file uses moving tags — so "the same tag" is not the same build, and checking the tag locally observes what it points at today rather than what the runner resolved.
|
|
68
|
+
- **@voltro/database, @voltro/plugin-versioning, @voltro/plugin-flags** — A versioned table whose NAME was long enough could not be written to at all.
|
|
69
|
+
|
|
70
|
+
`id()` is `VARCHAR(64)` on mysql / mariadb and `NVARCHAR(64)` on mssql, and unbounded `TEXT` on postgres and sqlite. The versioning recorder built its history key by concatenation — `rowver_<tableName>_<rowId>_<version>`, which is `42 + len(tableName)` characters for a 32-character row id — so a 22-character table name fit and a 23-character one produced `ERROR 1406 (22001): Data too long for column 'id' at row 1`. A recorder runs on EVERY write, so this was not a refused import: it was a table nobody could write to, on three of five dialects, at a boundary no one can see when naming a table.
|
|
71
|
+
|
|
72
|
+
`derivedRowId(prefix, …parts)` (`@voltro/database`) derives a deterministic key of CONSTANT width — `rowver_<32 hex>`, 39 characters whatever goes in — joined over a `\u0000` separator so the parts stay injective (a `_`-joined key cannot tell `('a_b','c')` from `('a','b_c')`). Widening the column was the alternative and moves the wall rather than removing it; `id()` is also every user table's PK type. Nothing legible is lost: every table deriving a key this way already stores the parts in their own columns.
|
|
73
|
+
|
|
74
|
+
The same construction was in `plugin-flags` (`flag_<key>`, over an unbounded user-chosen flag key) and is fixed with it. A guard scans framework sources for an `id:` composed by interpolation and requires the helper, with an allowlist whose entries each name why their parts cannot grow — and which fails if an entry stops matching.
|
|
75
|
+
|
|
76
|
+
Also fixed: the versioning suite's live coverage was postgres-only, and postgres is one of the two dialects where that column is unbounded, so it was structurally incapable of seeing this. `@voltro/sql-mysql` is a test devDep of `@voltro/plugin-versioning` now, with a mysql+mariadb case driving an ordinary insert and update against a 33-character table name.
|
|
77
|
+
- **@voltro/database, @voltro/cli** — Two gaps on the `--target api` path, both about a failure that is present and unreadable.
|
|
78
|
+
|
|
79
|
+
**The driver was unreachable behind a WRAPPED rejection.** `extractDbCause` unwrapped a `FiberFailure` at the root only, so one reached through a `.cause` link stopped the walk — it carries `stack`, `message` and `name` and nothing else, which is indistinguishable from "no driver under this". That is exactly the shape a failing write recorder produces: it rethrows `new Error(<what it was doing>, { cause: err })` where `err` is the rejection its own insert made. So the same database refusal classified where no recorder runs and degraded to the bare runtime rendering where one does — which is the difference between the direct importer and an import through a running app with versioning or audit on. The walk now unwraps at every link.
|
|
80
|
+
|
|
81
|
+
**And the refusal report was never printed on that transport.** A refusal that crossed HTTP arrives as a 500 whose message embeds the `RowsRefusedError` as JSON; the CLI printed that body raw. So the operator on the transport that exists for "the database is somewhere you cannot open a shell" got the one output that has to be triaged by hand — and tallying the capped row list is how a per-table distribution gets reported that is not the real one. `--target api` now prints the same report as the direct path, `byTable` line and cap notice included.
|
|
82
|
+
- **@voltro/data-transfer, @voltro/cli** — Two reporting defects that made a refused import unreadable, both of the shape "the payload is present and property access is not the way to it".
|
|
83
|
+
|
|
84
|
+
**A refusal lost its tag on the mode that raises it most.** `--mode replace` runs in one transaction by default, and rolling that back needs a rejection — which the atomic wrapper obtained by throwing `new Error(Cause.pretty(cause))`, a rendering rather than the failure. From there the typed error could not come back: it was re-wrapped as a `BundleError` carrying itself as text. So `Effect.catchTag('RowsRefusedError', …)` matched nothing on the default path, `ImportError`'s union was a claim that path could not honour, and the CLI's refusal report — which branches on the tag — printed nothing at all. The typed error is thrown and passed through now; `asImportError` is exported for callers who catch the rejection rather than the effect.
|
|
85
|
+
|
|
86
|
+
**And the report read the tag off a `FiberFailure`.** What `Effect.runPromise` rejects with does not expose `_tag` by property access, so the renderer took its "not my error" branch on every direct-path run while being wired, tested and correct — the test drove the renderer with the error object, which is not the shape the call site produces. A reported refusal now also ENDS the command instead of being rethrown into `fatal unhandled cli error`: a refusal is a condition with a named cause, not a framework defect.
|
|
87
|
+
|
|
88
|
+
**An api host is no longer reported as an unreachable database.** A connect failure carries an address, a port and an errno — the same shape a database driver's carries — and one global handler renders that shape, so `--target api --api-url https://…` against a stopped instance printed `the database is not reachable at <api-host>:443 … Configured by: DB_URL` with `DB_URL` not in play. The transport names its own failure now (`InstanceUnreachable`), and the database explainer declines an endpoint whose PORT cannot be a database — judged by port because a driver reports the resolved address, so a host comparison would silence the real message for anyone naming their database by hostname.
|
|
89
|
+
|
|
90
|
+
### Internal (no consumer-facing effect)
|
|
91
|
+
|
|
92
|
+
- **@voltro/sql-postgres** — A test teardown terminated connections its own pool was still closing, and the resulting error failed the RUN rather than any test.
|
|
93
|
+
|
|
94
|
+
`clusterColdStart` drops a per-run database, and the runners it spawned are killed with SIGKILL, so their backends never close — hence the deliberate `pg_terminate_backend` before the `DROP`. But `pool.end()` resolves once it has ASKED the pool to close, not once every socket is down, so the terminate could also land on a connection belonging to the test itself. `pg` reports that as an `error` event on the idle client, and an unhandled one takes down the process.
|
|
95
|
+
|
|
96
|
+
The shape it took on a release gate is the reason this is written down: **36 of 36 test files green, and the suite exiting 1.** Nothing points at the teardown — the failure is attributed to whichever suite happened to run last, which is a different one each time. A connection error while we are tearing the database down carries no signal, so it is handled where it arises.
|
|
29
97
|
|
|
30
|
-
|
|
98
|
+
Test-only; no product code changed.
|
|
99
|
+
- **@voltro/plugin-auth** — The TOTP skew-window test uses a fixed secret. Test-only; no product code changed, and the assertion is unchanged.
|
|
31
100
|
|
|
32
|
-
|
|
101
|
+
It failed once on a release gate — `expected true to be false`, meaning a code two steps outside the ±1 window verified. That is the shape of a security defect, so it was treated as one until measured:
|
|
33
102
|
|
|
34
|
-
|
|
103
|
+
- `TOTP_SKEW` is 1 and the verify loop checks exactly three counters, compared with `timingSafeEqual`; - `T0` is a constant and the clock is injected, so the only varying input was `generateTotpSecret()`; - over **50 000 fresh secrets**: zero collisions between the ±2 codes and the ±1 window (pure chance predicts ~0.3), zero degenerate secrets, uniform length; - **60 consecutive runs** of the file: green.
|
|
35
104
|
|
|
36
|
-
|
|
105
|
+
So the implementation is sound and that red was two 6-digit codes coinciding — about six in a million per run. Worth stating plainly: that makes the observed failure a one-in-167 000 event, which fits every measurement and is still remarkable. It was not reproduced.
|
|
37
106
|
|
|
38
|
-
The test
|
|
107
|
+
The fix is to remove the coin flip rather than to re-run until green. A random secret buys this test nothing — the property under test is the WIDTH of the window, which does not depend on which secret is used. It only buys a rare red that costs a diagnosis cycle and teaches the reader to re-run. Pinned, so the next failure there means the window moved.
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"dependencies": {
|
|
13
13
|
"@effect/platform": "^0.97.0",
|
|
14
14
|
"@effect/rpc": "^0.76.0",
|
|
15
|
-
"@voltro/ai": "0.
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/protocol": "0.
|
|
20
|
-
"@voltro/runtime": "0.
|
|
15
|
+
"@voltro/ai": "0.45.0",
|
|
16
|
+
"@voltro/cli": "0.45.0",
|
|
17
|
+
"@voltro/database": "0.45.0",
|
|
18
|
+
"@voltro/env": "0.45.0",
|
|
19
|
+
"@voltro/protocol": "0.45.0",
|
|
20
|
+
"@voltro/runtime": "0.45.0",
|
|
21
21
|
"effect": "^3.22.0"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@voltro/testing": "0.
|
|
24
|
+
"@voltro/testing": "0.45.0",
|
|
25
25
|
"typescript": "^6.0.3",
|
|
26
26
|
"@vitest/coverage-v8": "^4.1.10",
|
|
27
27
|
"vitest": "^4.1.10"
|
|
@@ -13,17 +13,17 @@
|
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@effect/platform": "^0.97.0",
|
|
15
15
|
"@effect/rpc": "^0.76.0",
|
|
16
|
-
"@voltro/cli": "0.
|
|
17
|
-
"@voltro/database": "0.
|
|
18
|
-
"@voltro/env": "0.
|
|
19
|
-
"@voltro/plugin-auth": "0.
|
|
20
|
-
"@voltro/protocol": "0.
|
|
21
|
-
"@voltro/runtime": "0.
|
|
22
|
-
"@voltro/sql-postgres": "0.
|
|
16
|
+
"@voltro/cli": "0.45.0",
|
|
17
|
+
"@voltro/database": "0.45.0",
|
|
18
|
+
"@voltro/env": "0.45.0",
|
|
19
|
+
"@voltro/plugin-auth": "0.45.0",
|
|
20
|
+
"@voltro/protocol": "0.45.0",
|
|
21
|
+
"@voltro/runtime": "0.45.0",
|
|
22
|
+
"@voltro/sql-postgres": "0.45.0",
|
|
23
23
|
"effect": "^3.22.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@voltro/testing": "0.
|
|
26
|
+
"@voltro/testing": "0.45.0",
|
|
27
27
|
"typescript": "^6.0.3",
|
|
28
28
|
"@vitest/coverage-v8": "^4.1.10",
|
|
29
29
|
"vitest": "^4.1.10"
|
|
@@ -16,16 +16,16 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@effect/platform": "^0.97.0",
|
|
18
18
|
"@effect/rpc": "^0.76.0",
|
|
19
|
-
"@voltro/cli": "0.
|
|
20
|
-
"@voltro/database": "0.
|
|
21
|
-
"@voltro/env": "0.
|
|
22
|
-
"@voltro/plugin-multitenancy": "0.
|
|
23
|
-
"@voltro/protocol": "0.
|
|
24
|
-
"@voltro/runtime": "0.
|
|
19
|
+
"@voltro/cli": "0.45.0",
|
|
20
|
+
"@voltro/database": "0.45.0",
|
|
21
|
+
"@voltro/env": "0.45.0",
|
|
22
|
+
"@voltro/plugin-multitenancy": "0.45.0",
|
|
23
|
+
"@voltro/protocol": "0.45.0",
|
|
24
|
+
"@voltro/runtime": "0.45.0",
|
|
25
25
|
"effect": "^3.22.0"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@voltro/testing": "0.
|
|
28
|
+
"@voltro/testing": "0.45.0",
|
|
29
29
|
"typescript": "^6.0.3",
|
|
30
30
|
"@vitest/coverage-v8": "^4.1.10",
|
|
31
31
|
"vitest": "^4.1.10"
|