@fuzdev/fuz_app 0.98.1 → 0.100.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/dist/db/CLAUDE.md CHANGED
@@ -128,14 +128,16 @@ DO NOTHING`), `_put_fact_refs`, `_get_fact` / `_get_fact_meta` / `_has_fact`
128
128
 
129
129
  FK dependency dictates order; consumers register via `migration_namespaces`
130
130
  on `create_app_backend` (after the built-in `fuz_auth` namespace). The fact
131
- namespace is independent (no FK to cell) but conventionally sits between cell
132
- and cell_history:
131
+ namespace is FK-independent so its slot is free; the spine binary and every
132
+ consumer place it last, keeping cell + its `cell_history` FK-child adjacent
133
+ (cross-namespace order is immaterial to the `schema_version` rows — `sequence`
134
+ is assigned per-namespace — so only the cell→cell_history FK constrains it):
133
135
 
134
136
  ```
135
137
  auth_migration_ns (built-in, fuz_auth)
136
- → CELL_MIGRATION_NS (fuz_cell — FKs actor.id)
137
- → FACT_MIGRATION_NS (fuz_facts — no external FK; optional)
138
+ → CELL_MIGRATION_NS (fuz_cell — FKs actor.id)
138
139
  → CELL_HISTORY_MIGRATION_NS (fuz_cell_history — FKs cell.id)
140
+ → FACT_MIGRATION_NS (fuz_facts — no external FK; slot is free)
139
141
  ```
140
142
 
141
143
  ### Pre-stable migration policy
@@ -27,9 +27,10 @@
27
27
  * `_at`-everywhere convention used by `account`, `actor`, `audit_log`,
28
28
  * `role_grant`, etc.
29
29
  *
30
- * **Single-migration shape**: `cell_v0` creates the canonical
30
+ * **Single-migration shape**: `full_cell_schema` creates the canonical
31
31
  * cell + cell_grant + cell_field + cell_item layout in one shot from the
32
- * live exported constants.
32
+ * live exported constants. The dormant `cell_history` table lives in the
33
+ * separate `fuz_cell_history` namespace (`cell_history_ddl.ts`).
33
34
  *
34
35
  * @module
35
36
  */
@@ -1 +1 @@
1
- {"version":3,"file":"cell_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkCG;AAGH,OAAO,KAAK,EAAC,SAAS,EAAE,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,oIAI1B,CAAC;AAER;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,4aAYtB,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,MAAM,CAYtC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,0iBAc5B,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAW5C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,+RAO5B,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAE5C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,qSAO3B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,MAAM,CAG3C,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,4DAA6D,CAAC;AAE3F,uBAAuB;AACvB,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAuB5C,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
1
+ {"version":3,"file":"cell_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,KAAK,EAAC,SAAS,EAAE,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,oIAI1B,CAAC;AAER;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,WAAW,4aAYtB,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,MAAM,CAYtC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,0iBAc5B,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAW5C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,+RAO5B,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAE5C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,qSAO3B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,MAAM,CAG3C,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,4DAA6D,CAAC;AAE3F,uBAAuB;AACvB,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAuB5C,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
@@ -27,9 +27,10 @@
27
27
  * `_at`-everywhere convention used by `account`, `actor`, `audit_log`,
28
28
  * `role_grant`, etc.
29
29
  *
30
- * **Single-migration shape**: `cell_v0` creates the canonical
30
+ * **Single-migration shape**: `full_cell_schema` creates the canonical
31
31
  * cell + cell_grant + cell_field + cell_item layout in one shot from the
32
- * live exported constants.
32
+ * live exported constants. The dormant `cell_history` table lives in the
33
+ * separate `fuz_cell_history` namespace (`cell_history_ddl.ts`).
33
34
  *
34
35
  * @module
35
36
  */
@@ -216,7 +217,7 @@ export const CELL_DROP_TABLES = ['cell_field', 'cell_item', 'cell_grant', 'cell'
216
217
  /** Cell migrations. */
217
218
  export const CELL_MIGRATIONS = [
218
219
  {
219
- name: 'cell_v0',
220
+ name: 'full_cell_schema',
220
221
  up: async (db) => {
221
222
  await db.query(CELL_VISIBILITY_TYPE);
222
223
  await db.query(CELL_SCHEMA);
@@ -43,7 +43,7 @@ export const CELL_HISTORY_DROP_TABLES = ['cell_history'];
43
43
  /** Cell-history migrations. */
44
44
  export const CELL_HISTORY_MIGRATIONS = [
45
45
  {
46
- name: 'cell_history_v0',
46
+ name: 'full_cell_history_schema',
47
47
  up: async (db) => {
48
48
  await db.query(CELL_HISTORY_SCHEMA);
49
49
  for (const sql of CELL_HISTORY_INDEXES) {
@@ -53,7 +53,7 @@ export const FACT_DROP_TABLES = ['memo', 'fact_ref', 'fact'];
53
53
  /** Fact + memo migrations. */
54
54
  export const FACT_MIGRATIONS = [
55
55
  {
56
- name: 'facts_v0',
56
+ name: 'full_fact_schema',
57
57
  up: async (db) => {
58
58
  await db.query(FACTS_SCHEMA);
59
59
  await db.query(FACT_REFS_SCHEMA);
@@ -6,6 +6,17 @@
6
6
  * `(namespace, name, sequence, applied_at)` — and the runner verifies the
7
7
  * applied list is a name-prefix of the code's migration array at boot.
8
8
  *
9
+ * **`name` is descriptive identity, not a version ordinal.** The runner orders
10
+ * by the `sequence` column and matches `name` by array position — it never
11
+ * parses the name — so `name` carries *what the migration is*
12
+ * (`full_cell_schema`, `role_grant_offer_and_scoped_role_grants`), and the
13
+ * redundant `_vN` ordinal is avoided (it would only duplicate `sequence`, and
14
+ * a `cell_v0` name tempts editing "v0" in place). `name` is half the
15
+ * `schema_version` PK (with `namespace`), so a rename is an identity change
16
+ * every already-migrated DB sees as `name-divergence-at-N`. This is also the
17
+ * cross-impl contract: the TS and Rust spines must record byte-identical
18
+ * `(namespace, name, sequence)` so a consumer can swap backends over one DB.
19
+ *
9
20
  * **Schema is not stabilized yet — append-only is NOT the rule.** While
10
21
  * fuz_app is pre-stable, migration bodies, names, and positions can change
11
22
  * freely between versions; consumers upgrading across a schema change are
@@ -1 +1 @@
1
- {"version":3,"file":"migrate.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/migrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4CG;AAIH,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAEhC;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAC7B;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAC3B,sBAAsB,GACtB,sBAAsB,GACtB,mBAAmB,GACnB,kBAAkB,GAClB,2BAA2B,GAC3B,4BAA4B,GAC5B,sCAAsC,CAAC;AAE1C,8DAA8D;AAC9D,MAAM,WAAW,qBAAqB;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;gBAEnC,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB;CAQtF;AA6ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,cAAc,GAC1B,IAAI,EAAE,EACN,YAAY,KAAK,CAAC,kBAAkB,CAAC,KACnC,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAuFhC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,QAAQ,GACpB,IAAI,EAAE,EACN,IAAI,kBAAkB,EACtB,OAAO,aAAa,CAAC,MAAM,CAAC,KAC1B,OAAO,CAAC,IAAI,CA+Dd,CAAC"}
1
+ {"version":3,"file":"migrate.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/migrate.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuDG;AAIH,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAEhC;;;;GAIG;AACH,MAAM,WAAW,SAAS;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;CAC7B;AAED,2DAA2D;AAC3D,MAAM,WAAW,eAAe;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,aAAa,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAC3B,sBAAsB,GACtB,sBAAsB,GACtB,mBAAmB,GACnB,kBAAkB,GAClB,2BAA2B,GAC3B,4BAA4B,GAC5B,sCAAsC,CAAC;AAE1C,8DAA8D;AAC9D,MAAM,WAAW,qBAAqB;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IACtC,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED;;;;GAIG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACxC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;gBAEnC,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB;CAQtF;AA6ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,cAAc,GAC1B,IAAI,EAAE,EACN,YAAY,KAAK,CAAC,kBAAkB,CAAC,KACnC,OAAO,CAAC,KAAK,CAAC,eAAe,CAAC,CAuFhC,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,eAAO,MAAM,QAAQ,GACpB,IAAI,EAAE,EACN,IAAI,kBAAkB,EACtB,OAAO,aAAa,CAAC,MAAM,CAAC,KAC1B,OAAO,CAAC,IAAI,CA+Dd,CAAC"}
@@ -6,6 +6,17 @@
6
6
  * `(namespace, name, sequence, applied_at)` — and the runner verifies the
7
7
  * applied list is a name-prefix of the code's migration array at boot.
8
8
  *
9
+ * **`name` is descriptive identity, not a version ordinal.** The runner orders
10
+ * by the `sequence` column and matches `name` by array position — it never
11
+ * parses the name — so `name` carries *what the migration is*
12
+ * (`full_cell_schema`, `role_grant_offer_and_scoped_role_grants`), and the
13
+ * redundant `_vN` ordinal is avoided (it would only duplicate `sequence`, and
14
+ * a `cell_v0` name tempts editing "v0" in place). `name` is half the
15
+ * `schema_version` PK (with `namespace`), so a rename is an identity change
16
+ * every already-migrated DB sees as `name-divergence-at-N`. This is also the
17
+ * cross-impl contract: the TS and Rust spines must record byte-identical
18
+ * `(namespace, name, sequence)` so a consumer can swap backends over one DB.
19
+ *
9
20
  * **Schema is not stabilized yet — append-only is NOT the rule.** While
10
21
  * fuz_app is pre-stable, migration bodies, names, and positions can change
11
22
  * freely between versions; consumers upgrading across a schema change are
@@ -14,8 +14,11 @@
14
14
  * (filesystem-backed `file:<shard>/<rest>` URLs) either return an
15
15
  * `X-Accel-Redirect` header pointing into nginx's internal facts location
16
16
  * (production) or stream from disk via the filesystem `FactExternalFetcher`
17
- * (dev / tests). The runtime mode is selected by the optional
18
- * `x_accel_redirect_prefix` factory option — set in prod, unset in dev.
17
+ * (dev / tests). The runtime mode is selected by the optional `x_accel`
18
+ * factory option — a validated `XAccelConfig` (built via
19
+ * `create_x_accel_config` in `server/x_accel.ts`) set in prod, unset in dev.
20
+ * The handle can only be obtained by proving the facts nginx location is
21
+ * `internal;`, so X-Accel serving can't be enabled against a public location.
19
22
  *
20
23
  * REST, not RPC: binary responses don't fit the JSON-RPC envelope.
21
24
  *
@@ -87,6 +90,7 @@
87
90
  import type { Logger } from '@fuzdev/fuz_util/log.ts';
88
91
  import { type RouteSpec } from '../http/route_spec.js';
89
92
  import type { AppDeps } from '../auth/deps.js';
93
+ import type { XAccelConfig } from './x_accel.js';
90
94
  export interface CreateServeFactRouteSpecOptions {
91
95
  /**
92
96
  * App deps reference. Currently unused at handler time (cell + fact
@@ -100,12 +104,13 @@ export interface CreateServeFactRouteSpecOptions {
100
104
  facts_dir: string;
101
105
  /**
102
106
  * When set, external facts return an `X-Accel-Redirect` pointing at
103
- * `${prefix}<shard>/<rest>` — nginx's internal facts location serves
104
- * the bytes. When unset, external facts stream from
105
- * `<facts_dir>/<shard>/<rest>` directly. Production sets this (e.g.
106
- * `/_facts/`); tests + dev leave it unset.
107
+ * `${x_accel.redirect_prefix}<shard>/<rest>` — nginx's internal facts
108
+ * location serves the bytes. When unset, external facts stream from
109
+ * `<facts_dir>/<shard>/<rest>` directly. Production sets a validated
110
+ * `XAccelConfig` (built via `create_x_accel_config`, which proves the
111
+ * facts location is `internal;`); tests + dev leave it unset.
107
112
  */
108
- x_accel_redirect_prefix?: string;
113
+ x_accel?: XAccelConfig;
109
114
  log: Logger;
110
115
  }
111
116
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqFG;AAMH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAepD,OAAO,EAAsC,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAkE7C,MAAM,WAAW,+BAA+B;IAC/C;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;OAMG;IACH,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,GAAG,EAAE,MAAM,CAAC;CACZ;AAgHD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SA8CF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SAgCF,CAAC"}
1
+ {"version":3,"file":"serve_fact_route.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/serve_fact_route.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwFG;AAMH,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,yBAAyB,CAAC;AAepD,OAAO,EAAsC,KAAK,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAE1F,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,iBAAiB,CAAC;AAM7C,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,cAAc,CAAC;AA6D/C,MAAM,WAAW,+BAA+B;IAC/C;;;;;;OAMG;IACH,IAAI,EAAE,OAAO,CAAC;IACd,kFAAkF;IAClF,SAAS,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,GAAG,EAAE,MAAM,CAAC;CACZ;AAgHD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iCAAiC,GAC7C,SAAS,+BAA+B,KACtC,SA8CF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,4BAA4B,GACxC,SAAS,+BAA+B,KACtC,SAgCF,CAAC"}
@@ -14,8 +14,11 @@
14
14
  * (filesystem-backed `file:<shard>/<rest>` URLs) either return an
15
15
  * `X-Accel-Redirect` header pointing into nginx's internal facts location
16
16
  * (production) or stream from disk via the filesystem `FactExternalFetcher`
17
- * (dev / tests). The runtime mode is selected by the optional
18
- * `x_accel_redirect_prefix` factory option — set in prod, unset in dev.
17
+ * (dev / tests). The runtime mode is selected by the optional `x_accel`
18
+ * factory option — a validated `XAccelConfig` (built via
19
+ * `create_x_accel_config` in `server/x_accel.ts`) set in prod, unset in dev.
20
+ * The handle can only be obtained by proving the facts nginx location is
21
+ * `internal;`, so X-Accel serving can't be enabled against a public location.
19
22
  *
20
23
  * REST, not RPC: binary responses don't fit the JSON-RPC envelope.
21
24
  *
@@ -167,7 +170,7 @@ const params_400_error = {
167
170
  * fact is genuinely absent or merely unviewable.
168
171
  */
169
172
  const serve_fact_bytes = async (c, route, hash, config) => {
170
- const { facts_dir, x_accel_redirect_prefix, log } = config;
173
+ const { facts_dir, x_accel, log } = config;
171
174
  const meta = await query_get_fact_meta({ db: route.db }, hash);
172
175
  if (!meta) {
173
176
  return c.body(null, 404);
@@ -198,13 +201,13 @@ const serve_fact_bytes = async (c, route, hash, config) => {
198
201
  return c.body(null, 404);
199
202
  }
200
203
  const { shard, rest } = parsed;
201
- if (x_accel_redirect_prefix !== undefined) {
202
- // Production: hand off to nginx via the internal facts location.
204
+ if (x_accel !== undefined) {
205
+ // Production: hand off to nginx via the validated `internal;` facts location.
203
206
  return c.body(null, 200, {
204
207
  'Content-Type': content_type,
205
208
  'Content-Length': size,
206
209
  'Cache-Control': CACHE_CONTROL,
207
- 'X-Accel-Redirect': `${x_accel_redirect_prefix}${shard}/${rest}`,
210
+ 'X-Accel-Redirect': `${x_accel.redirect_prefix}${shard}/${rest}`,
208
211
  ...FACT_SECURITY_HEADERS,
209
212
  });
210
213
  }
@@ -252,8 +255,8 @@ const build_public_request_context = async (c, route) => {
252
255
  * from `c.var.account_id` and enforces visibility per-reference.
253
256
  */
254
257
  export const create_serve_cell_fact_route_spec = (options) => {
255
- const { facts_dir, x_accel_redirect_prefix, log } = options;
256
- const config = { facts_dir, x_accel_redirect_prefix, log };
258
+ const { facts_dir, x_accel, log } = options;
259
+ const config = { facts_dir, x_accel, log };
257
260
  return {
258
261
  method: 'GET',
259
262
  path: '/api/cells/:cell_id/facts/:hash',
@@ -310,8 +313,8 @@ export const create_serve_cell_fact_route_spec = (options) => {
310
313
  * hash to a non-admin.
311
314
  */
312
315
  export const create_serve_fact_route_spec = (options) => {
313
- const { facts_dir, x_accel_redirect_prefix, log } = options;
314
- const config = { facts_dir, x_accel_redirect_prefix, log };
316
+ const { facts_dir, x_accel, log } = options;
317
+ const config = { facts_dir, x_accel, log };
315
318
  return {
316
319
  method: 'GET',
317
320
  path: '/api/facts/:hash',
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Fail-loud validation of the X-Accel facts nginx location.
3
+ *
4
+ * The `X-Accel-Redirect` serving path's confidentiality depends on the facts
5
+ * nginx location being `internal;` — only the authz'd handler's redirect (an
6
+ * internal subrequest) may reach it. A *public* facts location would serve any
7
+ * fact's bytes to anyone who guesses the `<shard>/<rest>` path, bypassing every
8
+ * cell-visibility check. `XAccelConfig` makes that assertion structural: a
9
+ * consumer can only obtain the redirect prefix by building an `XAccelConfig`,
10
+ * which runs this check and throws **loudly** on a missing or non-`internal;`
11
+ * location — it cannot silently fall closed.
12
+ *
13
+ * This is a best-effort string check (brace-matched `location` blocks), the TS
14
+ * twin of the Rust `fuz_fact_serving` `nginx.rs` — not a real nginx parser, but
15
+ * it catches the security-critical omission. Distinct from
16
+ * `validate_nginx_config` (`server/validate_nginx.ts`), which validates the
17
+ * `/api` Authorization-strip + security headers and takes no facts-location
18
+ * path.
19
+ *
20
+ * @module
21
+ */
22
+ import { z } from 'zod';
23
+ /** Result of the facts-location check. */
24
+ export interface NginxFactsValidation {
25
+ /** `true` when the facts location exists and is `internal;`. */
26
+ ok: boolean;
27
+ /** Fatal issues — a missing or non-`internal` facts location. */
28
+ errors: Array<string>;
29
+ }
30
+ /**
31
+ * Assert the nginx `location` serving the X-Accel facts prefix is `internal;`.
32
+ *
33
+ * `facts_location` is the path the X-Accel redirect prefix points at (e.g.
34
+ * `/_facts/`). Returns a fatal error when no matching `location` block exists,
35
+ * or when the matching block is not marked `internal;` — either is a public
36
+ * facts location that bypasses cell visibility.
37
+ *
38
+ * @param config - the nginx config template string to check
39
+ * @param facts_location - the facts location path the redirect prefix points at
40
+ * @returns `{ok, errors}` — `ok` is `true` only when the location exists and is `internal;`
41
+ */
42
+ export declare const validate_facts_internal_location: (config: string, facts_location: string) => NginxFactsValidation;
43
+ /**
44
+ * A validated X-Accel redirect configuration — the only handle that enables the
45
+ * `X-Accel-Redirect` serving path in `server/serve_fact_route.ts`.
46
+ *
47
+ * The redirect prefix can be obtained **only** by passing the nginx config
48
+ * through `validate_facts_internal_location` (via `create_x_accel_config`), so
49
+ * X-Accel serving is impossible to enable without proving the facts location is
50
+ * `internal;` at boot — a public facts location would bypass every
51
+ * cell-visibility check. A Zod-branded type: the brand can't be forged without
52
+ * an explicit cast, so the factory is the only ordinary construction path.
53
+ */
54
+ export declare const XAccelConfig: z.core.$ZodBranded<z.ZodObject<{
55
+ /** The validated redirect prefix the serving path prepends (e.g. `/_facts/`). */
56
+ redirect_prefix: z.ZodString;
57
+ }, z.core.$strict>, "XAccelConfig", "out">;
58
+ export type XAccelConfig = z.infer<typeof XAccelConfig>;
59
+ /**
60
+ * A misconfigured X-Accel facts location — a fail-loud boot error thrown by
61
+ * `create_x_accel_config`.
62
+ */
63
+ export declare class XAccelConfigError extends Error {
64
+ /** The validator errors that made the location unsafe. */
65
+ readonly errors: Array<string>;
66
+ constructor(errors: Array<string>);
67
+ }
68
+ /**
69
+ * Build a validated `XAccelConfig`, asserting the nginx `location` serving
70
+ * `redirect_prefix` is `internal;`.
71
+ *
72
+ * The make-impossible-states gate: serving can only emit `X-Accel-Redirect`
73
+ * into a location proven `internal;` at boot.
74
+ *
75
+ * @param redirect_prefix - the X-Accel redirect prefix (e.g. `/_facts/`)
76
+ * @param nginx_config - the nginx config template string to validate against
77
+ * @returns the validated `XAccelConfig` carrying `redirect_prefix`
78
+ * @throws `XAccelConfigError` when `nginx_config` has no matching `location` for
79
+ * `redirect_prefix`, or the matching block is not marked `internal;` — either
80
+ * is a public facts location that bypasses every cell-visibility check.
81
+ */
82
+ export declare const create_x_accel_config: (redirect_prefix: string, nginx_config: string) => XAccelConfig;
83
+ //# sourceMappingURL=x_accel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"x_accel.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/x_accel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,0CAA0C;AAC1C,MAAM,WAAW,oBAAoB;IACpC,gEAAgE;IAChE,EAAE,EAAE,OAAO,CAAC;IACZ,iEAAiE;IACjE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAuDD;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,gCAAgC,GAC5C,QAAQ,MAAM,EACd,gBAAgB,MAAM,KACpB,oBAqBF,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY;IAEvB,iFAAiF;;0CAG5D,CAAC;AACxB,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,YAAY,CAAC,CAAC;AAExD;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC3C,0DAA0D;IAC1D,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;gBACnB,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC;CAKjC;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qBAAqB,GACjC,iBAAiB,MAAM,EACvB,cAAc,MAAM,KAClB,YAMF,CAAC"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Fail-loud validation of the X-Accel facts nginx location.
3
+ *
4
+ * The `X-Accel-Redirect` serving path's confidentiality depends on the facts
5
+ * nginx location being `internal;` — only the authz'd handler's redirect (an
6
+ * internal subrequest) may reach it. A *public* facts location would serve any
7
+ * fact's bytes to anyone who guesses the `<shard>/<rest>` path, bypassing every
8
+ * cell-visibility check. `XAccelConfig` makes that assertion structural: a
9
+ * consumer can only obtain the redirect prefix by building an `XAccelConfig`,
10
+ * which runs this check and throws **loudly** on a missing or non-`internal;`
11
+ * location — it cannot silently fall closed.
12
+ *
13
+ * This is a best-effort string check (brace-matched `location` blocks), the TS
14
+ * twin of the Rust `fuz_fact_serving` `nginx.rs` — not a real nginx parser, but
15
+ * it catches the security-critical omission. Distinct from
16
+ * `validate_nginx_config` (`server/validate_nginx.ts`), which validates the
17
+ * `/api` Authorization-strip + security headers and takes no facts-location
18
+ * path.
19
+ *
20
+ * @module
21
+ */
22
+ import { z } from 'zod';
23
+ /** Strip every trailing `/` so `/_facts/` and `/_facts` compare equal. */
24
+ const trim_trailing_slashes = (value) => value.replace(/\/+$/, '');
25
+ /** `true` if a location block body contains an `internal` directive. */
26
+ const is_internal = (content) => content.split(/[;\s]+/).some((token) => token === 'internal');
27
+ /**
28
+ * Extract `{path, content}` for each `location … { … }` block, brace-matched so
29
+ * nested blocks are kept whole. `path` is the last whitespace token before the
30
+ * opening brace (the location path, after any modifier). Best-effort: a stray
31
+ * `location` token in a comment can produce a spurious block, harmless for this
32
+ * check.
33
+ */
34
+ const find_location_blocks = (config) => {
35
+ const blocks = [];
36
+ let search = 0;
37
+ let kw;
38
+ while ((kw = config.indexOf('location', search)) !== -1) {
39
+ // Require a token boundary before `location` (start or whitespace) so
40
+ // `relocation` etc. don't match.
41
+ const boundary_ok = kw === 0 || /\s/.test(config[kw - 1]);
42
+ const after = kw + 'location'.length;
43
+ const brace = config.indexOf('{', after);
44
+ if (brace === -1)
45
+ break;
46
+ if (!boundary_ok) {
47
+ search = after;
48
+ continue;
49
+ }
50
+ const header = config.slice(after, brace).trim();
51
+ const tokens = header.split(/\s+/).filter((token) => token.length > 0);
52
+ const path = tokens.length > 0 ? tokens[tokens.length - 1] : '';
53
+ // Brace-match the body.
54
+ let depth = 1;
55
+ let i = brace + 1;
56
+ while (i < config.length && depth > 0) {
57
+ const ch = config[i];
58
+ if (ch === '{')
59
+ depth++;
60
+ else if (ch === '}')
61
+ depth--;
62
+ i++;
63
+ }
64
+ blocks.push({ path, content: config.slice(brace, i) });
65
+ search = i;
66
+ }
67
+ return blocks;
68
+ };
69
+ /**
70
+ * Assert the nginx `location` serving the X-Accel facts prefix is `internal;`.
71
+ *
72
+ * `facts_location` is the path the X-Accel redirect prefix points at (e.g.
73
+ * `/_facts/`). Returns a fatal error when no matching `location` block exists,
74
+ * or when the matching block is not marked `internal;` — either is a public
75
+ * facts location that bypasses cell visibility.
76
+ *
77
+ * @param config - the nginx config template string to check
78
+ * @param facts_location - the facts location path the redirect prefix points at
79
+ * @returns `{ok, errors}` — `ok` is `true` only when the location exists and is `internal;`
80
+ */
81
+ export const validate_facts_internal_location = (config, facts_location) => {
82
+ const errors = [];
83
+ const normalized = trim_trailing_slashes(facts_location);
84
+ const blocks = find_location_blocks(config);
85
+ const matching = blocks.filter((block) => trim_trailing_slashes(block.path) === normalized);
86
+ if (matching.length === 0) {
87
+ errors.push(`No nginx \`location ${facts_location}\` block found — the X-Accel facts location must ` +
88
+ `exist and be marked \`internal;\` so only the authz'd handler's redirect can reach it. ` +
89
+ `A public facts location bypasses every cell-visibility check.`);
90
+ }
91
+ else if (!matching.some((block) => is_internal(block.content))) {
92
+ errors.push(`nginx \`location ${facts_location}\` is NOT marked \`internal;\` — a public facts ` +
93
+ `location serves any fact's bytes to anyone who guesses the path, bypassing every ` +
94
+ `cell-visibility check. Add \`internal;\` to the block.`);
95
+ }
96
+ return { ok: errors.length === 0, errors };
97
+ };
98
+ /**
99
+ * A validated X-Accel redirect configuration — the only handle that enables the
100
+ * `X-Accel-Redirect` serving path in `server/serve_fact_route.ts`.
101
+ *
102
+ * The redirect prefix can be obtained **only** by passing the nginx config
103
+ * through `validate_facts_internal_location` (via `create_x_accel_config`), so
104
+ * X-Accel serving is impossible to enable without proving the facts location is
105
+ * `internal;` at boot — a public facts location would bypass every
106
+ * cell-visibility check. A Zod-branded type: the brand can't be forged without
107
+ * an explicit cast, so the factory is the only ordinary construction path.
108
+ */
109
+ export const XAccelConfig = z
110
+ .strictObject({
111
+ /** The validated redirect prefix the serving path prepends (e.g. `/_facts/`). */
112
+ redirect_prefix: z.string(),
113
+ })
114
+ .brand('XAccelConfig');
115
+ /**
116
+ * A misconfigured X-Accel facts location — a fail-loud boot error thrown by
117
+ * `create_x_accel_config`.
118
+ */
119
+ export class XAccelConfigError extends Error {
120
+ /** The validator errors that made the location unsafe. */
121
+ errors;
122
+ constructor(errors) {
123
+ super(`X-Accel facts location is not safely \`internal;\`: ${errors.join('; ')}`);
124
+ this.name = 'XAccelConfigError';
125
+ this.errors = errors;
126
+ }
127
+ }
128
+ /**
129
+ * Build a validated `XAccelConfig`, asserting the nginx `location` serving
130
+ * `redirect_prefix` is `internal;`.
131
+ *
132
+ * The make-impossible-states gate: serving can only emit `X-Accel-Redirect`
133
+ * into a location proven `internal;` at boot.
134
+ *
135
+ * @param redirect_prefix - the X-Accel redirect prefix (e.g. `/_facts/`)
136
+ * @param nginx_config - the nginx config template string to validate against
137
+ * @returns the validated `XAccelConfig` carrying `redirect_prefix`
138
+ * @throws `XAccelConfigError` when `nginx_config` has no matching `location` for
139
+ * `redirect_prefix`, or the matching block is not marked `internal;` — either
140
+ * is a public facts location that bypasses every cell-visibility check.
141
+ */
142
+ export const create_x_accel_config = (redirect_prefix, nginx_config) => {
143
+ const validation = validate_facts_internal_location(nginx_config, redirect_prefix);
144
+ if (!validation.ok) {
145
+ throw new XAccelConfigError(validation.errors);
146
+ }
147
+ return XAccelConfig.parse({ redirect_prefix });
148
+ };
@@ -214,6 +214,7 @@ test-only by construction.
214
214
 
215
215
  - `query_schema_snapshot(db, options?)` — introspects a live DB into a deterministic `SchemaSnapshot` via `pg_catalog` + `information_schema`. Captures tables, columns (with `udt_name` to distinguish int4/int8), indexes (`indexdef`), constraints (`pg_get_constraintdef`), sequences, and enum types (`pg_enum` labels in declared `enumsortorder`, so a `cell_visibility` label-set/order drift is gated). The `schema_version` migration tracker is always excluded — it's framework bookkeeping, not domain schema, and impls organize migration namespaces differently. Twinned by `fuz_db::query_schema_snapshot` (Rust); the `_testing_schema_snapshot` RPC's wire validator is the shared `SchemaSnapshot`, so the enum field must serialize on both sides.
216
216
  - `SchemaSnapshot` — the Zod schema is canonical (co-located in `schema_introspect.ts`; the cross-impl `_testing_schema_snapshot` RPC action reuses it as its wire validator, and the type is `z.infer`'d from it). Fully JSON-serializable; every collection deterministically sorted on capture so structural equality is stable across runs.
217
+ - `query_migration_tracker(db)` / `MigrationTracker` — the migration-_identity_ twin of the snapshot: reads the `schema_version` tracker rows (`[{namespace, name, sequence}]`) the snapshot deliberately _excludes_. `MigrationTracker` is the `_testing_migration_tracker` RPC's wire validator; pairs with `assert_migration_trackers_equal` to gate that the two spines record byte-identical migration identity (the swap-freely invariant), catching a migration-name or namespace-partitioning drift the provenance-agnostic schema snapshot can't see.
217
218
 
218
219
  ### `schema_parity.ts` — `assert_schema_snapshots_equal`
219
220
 
@@ -221,6 +222,7 @@ test-only by construction.
221
222
  - `format_schema_diffs(diffs, labels?)` — human-readable multi-line rendering; labels name the impl on each side (e.g., `{a: 'deno', b: 'rust'}`).
222
223
  - `assert_schema_snapshots_equal(a, b, labels?)` — throws on drift with a fully-formatted diff message.
223
224
  - `SchemaDiff` — tagged-union per drift kind: `table_only_in`, `column_only_in`, `column_field_differs`, `index_only_in`, `index_definition_differs`, `constraint_only_in`, `constraint_differs`, `sequence_only_in`, `sequence_data_type_differs`, `enum_only_in`, `enum_labels_differ` (enum labels compared positionally — declared order is significant).
225
+ - `diff_migration_trackers(a, b)` / `format_migration_tracker_diffs(diffs, labels?)` / `assert_migration_trackers_equal(a, b, labels?)` — the migration-identity twins of the snapshot diff/format/assert, over two `MigrationTracker`s. `MigrationTrackerDiff` is keyed on `(namespace, name)` (the `schema_version` PK): `tracker_row_only_in` (a migration rename or namespace re-partition) and `tracker_sequence_differs` (a re-order).
224
226
 
225
227
  fuz_app's own spine gates this cross-process via the `cross_backend_parity` project (`schema_parity.cross.test.ts` + the dual-spawn `global_setup_schema_parity.ts`), diffing the TS spine ↔ `testing_spine_stub` full schema — `npm run test:cross:parity`. The forge has its own deno↔rust parity gate.
226
228
 
@@ -3,7 +3,7 @@ import { Uuid } from '@fuzdev/fuz_util/id.ts';
3
3
  import type { TestAccount } from '../app_server.js';
4
4
  import type { BackendCapabilities } from './capabilities.js';
5
5
  import { type FetchTransport } from '../transports/fetch_transport.js';
6
- import type { SchemaSnapshot } from '../schema_introspect.js';
6
+ import { MigrationTracker, type SchemaSnapshot } from '../schema_introspect.js';
7
7
  import { ActionManifest } from './action_manifest.js';
8
8
  import type { BackendHandle } from './spawn_backend.js';
9
9
  /**
@@ -406,6 +406,18 @@ export declare const capture_schema_snapshot: (handle: ReconstructedBootstrapped
406
406
  * `fuz_testing` mirror); the normalized shapes match by design.
407
407
  */
408
408
  export declare const capture_action_manifest: (handle: ReconstructedBootstrappedBackendHandle) => Promise<ActionManifest>;
409
+ /**
410
+ * Capture a backend's `schema_version` migration tracker over the
411
+ * `_testing_migration_tracker` RPC action (keeper daemon-token channel). The
412
+ * provenance twin of `capture_schema_snapshot` — where that reads the
413
+ * resulting schema (and excludes the tracker), this reads the tracker rows
414
+ * themselves. Pair two calls with `assert_migration_trackers_equal`
415
+ * (`testing/schema_parity.ts`) to gate that the TS spine and the Rust
416
+ * `testing_spine_stub` record byte-identical migration identity
417
+ * (`namespace` + `name` + `sequence`) — the invariant the schema-snapshot
418
+ * gate is blind to.
419
+ */
420
+ export declare const capture_migration_tracker: (handle: ReconstructedBootstrappedBackendHandle) => Promise<MigrationTracker>;
409
421
  /**
410
422
  * Backdating offset (seconds) the `mint_expired_session` seam passes to
411
423
  * `mint_test_session` / `_testing_mint_session`. A minute in the past is
@@ -1 +1 @@
1
- {"version":3,"file":"setup.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/setup.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"setup.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/setup.ts"],"names":[],"mappings":"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"}
@@ -27,6 +27,7 @@ import { DAEMON_TOKEN_HEADER } from '../../auth/daemon_token.js';
27
27
  import { USERNAME_LENGTH_MAX } from '../../primitive_schemas.js';
28
28
  import { DEFAULT_TEST_PASSWORD } from '../test_credentials.js';
29
29
  import { create_fetch_transport } from '../transports/fetch_transport.js';
30
+ import { MigrationTracker } from '../schema_introspect.js';
30
31
  import { ActionManifest } from './action_manifest.js';
31
32
  /**
32
33
  * Build an `ExtraAccountFixture` from a seeded `{account, actor,
@@ -179,6 +180,22 @@ export const capture_schema_snapshot = async (handle, options = {}) => {
179
180
  // reconcile first, so it's left as a follow-up.
180
181
  return raw;
181
182
  };
183
+ /**
184
+ * Capture a no-arg daemon-token introspection RPC and validate the wire
185
+ * response against `schema`. The shared shape behind the parse-based
186
+ * `capture_*` cross-impl parity probes: call a `_testing_*` introspection
187
+ * action over the keeper's daemon-token channel, then `parse` so a shape
188
+ * drift between the TS and Rust answers fails loud at the boundary.
189
+ *
190
+ * (`capture_schema_snapshot` deliberately stays separate: it *casts* rather
191
+ * than parses — narrowing a large snapshot to the Zod model would silently
192
+ * drop unmodeled fields the parity diff should still see — and it takes an
193
+ * `exclude_tables` param.)
194
+ */
195
+ const capture_via_daemon = async (handle, method, schema) => {
196
+ const raw = await rpc_via_transport(handle.keeper_daemon_transport, handle.config.rpc_path, method, {}, handle.config.name, { [DAEMON_TOKEN_HEADER]: handle.daemon_token });
197
+ return schema.parse(raw);
198
+ };
182
199
  /**
183
200
  * Capture a backend's live RPC action manifest over the
184
201
  * `_testing_action_manifest` RPC action (keeper daemon-token channel). The
@@ -189,10 +206,19 @@ export const capture_schema_snapshot = async (handle, options = {}) => {
189
206
  * own live registry (TS via `build_action_manifest`, Rust via the
190
207
  * `fuz_testing` mirror); the normalized shapes match by design.
191
208
  */
192
- export const capture_action_manifest = async (handle) => {
193
- const raw = await rpc_via_transport(handle.keeper_daemon_transport, handle.config.rpc_path, '_testing_action_manifest', {}, handle.config.name, { [DAEMON_TOKEN_HEADER]: handle.daemon_token });
194
- return ActionManifest.parse(raw);
195
- };
209
+ export const capture_action_manifest = async (handle) => capture_via_daemon(handle, '_testing_action_manifest', ActionManifest);
210
+ /**
211
+ * Capture a backend's `schema_version` migration tracker over the
212
+ * `_testing_migration_tracker` RPC action (keeper daemon-token channel). The
213
+ * provenance twin of `capture_schema_snapshot` — where that reads the
214
+ * resulting schema (and excludes the tracker), this reads the tracker rows
215
+ * themselves. Pair two calls with `assert_migration_trackers_equal`
216
+ * (`testing/schema_parity.ts`) to gate that the TS spine and the Rust
217
+ * `testing_spine_stub` record byte-identical migration identity
218
+ * (`namespace` + `name` + `sequence`) — the invariant the schema-snapshot
219
+ * gate is blind to.
220
+ */
221
+ export const capture_migration_tracker = async (handle) => capture_via_daemon(handle, '_testing_migration_tracker', MigrationTracker);
196
222
  /**
197
223
  * Backdating offset (seconds) the `mint_expired_session` seam passes to
198
224
  * `mint_test_session` / `_testing_mint_session`. A minute in the past is
@@ -1 +1 @@
1
- {"version":3,"file":"testing_backdoor.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_backdoor.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA0D9B,OAAO,KAAK,EAAC,wBAAwB,EAAc,MAAM,YAAY,CAAC;AAkEtE,kEAAkE;AAClE,MAAM,MAAM,+BAA+B,GAAG,wBAAwB,CAAC;AAEvE,eAAO,MAAM,qCAAqC,GACjD,SAAS,+BAA+B,KACtC,IAiCF,CAAC"}
1
+ {"version":3,"file":"testing_backdoor.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_backdoor.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AA2D9B,OAAO,KAAK,EAAC,wBAAwB,EAAc,MAAM,YAAY,CAAC;AAqEtE,kEAAkE;AAClE,MAAM,MAAM,+BAA+B,GAAG,wBAAwB,CAAC;AAEvE,eAAO,MAAM,qCAAqC,GACjD,SAAS,+BAA+B,KACtC,IAiCF,CAAC"}
@@ -4,10 +4,11 @@ import '../assert_dev_env.js';
4
4
  * actions.
5
5
  *
6
6
  * `_testing_reset` / `_testing_mint_session` / `_testing_put_fact` /
7
- * `_testing_schema_snapshot` / `_testing_action_manifest` are privileged
8
- * test-binary actions the production wire never exposes — three direct DB
9
- * writes (full auth wipe, forged session row, raw fact insert) plus two
10
- * introspection reads (the live schema + the live RPC registry, the highest
7
+ * `_testing_schema_snapshot` / `_testing_migration_tracker` /
8
+ * `_testing_action_manifest` are privileged test-binary actions the production
9
+ * wire never exposes — three direct DB writes (full auth wipe, forged session
10
+ * row, raw fact insert) plus three introspection reads (the live schema, the
11
+ * `schema_version` migration tracker, and the live RPC registry — the highest
11
12
  * info-leak of the set were the gate to break). Their only structural fence
12
13
  * is the **daemon-token** credential gate on
13
14
  * each spec's `auth` axis. A test binary live-mounts them on its RPC
@@ -67,6 +68,9 @@ const backdoor_methods = [
67
68
  // The schema-dump read — `exclude_tables` is optional, so `{}` is valid
68
69
  // and clears the 400 phase like the writes above.
69
70
  { method: '_testing_schema_snapshot', params: {} },
71
+ // The migration-tracker-dump read — input is an empty strict object, so `{}`
72
+ // is valid and clears the 400 phase, reaching the credential gate like the rest.
73
+ { method: '_testing_migration_tracker', params: {} },
70
74
  // The RPC-registry-dump read — input is an empty strict object, so `{}` is
71
75
  // valid and clears the 400 phase, reaching the credential gate like the rest.
72
76
  { method: '_testing_action_manifest', params: {} },
@@ -2,15 +2,18 @@ import '../assert_dev_env.js';
2
2
  /**
3
3
  * Test-binary RPC actions for cross-process integration tests.
4
4
  *
5
- * Five daemon-token-authed actions, bundled by `create_testing_actions`:
5
+ * Six daemon-token-authed actions, bundled by `create_testing_actions`:
6
6
  * **`_testing_reset`** (DB wipe + keeper re-seed), **`_testing_drain_effects`**
7
7
  * (audit barrier), **`_testing_mint_session`** (forge an
8
8
  * expired-by-construction server-side session for the expiry conformance
9
9
  * cases), **`_testing_put_fact`** (seed an embedded fact for the fact-serving
10
- * suite), and **`_testing_schema_snapshot`** (introspect the live schema for
11
- * cross-impl parity diffing against a Rust backend's `fuz_db` snapshot).
10
+ * suite), **`_testing_schema_snapshot`** (introspect the live schema for
11
+ * cross-impl parity diffing against a Rust backend's `fuz_db` snapshot), and
12
+ * **`_testing_migration_tracker`** (dump the `schema_version` tracker rows for
13
+ * cross-impl migration-identity parity — the provenance half the snapshot gate
14
+ * excludes by design).
12
15
  *
13
- * A sixth daemon-token action, **`_testing_action_manifest`** (dump the live
16
+ * A further daemon-token action, **`_testing_action_manifest`** (dump the live
14
17
  * RPC registry for cross-impl method-set + auth-shape parity), lives here too
15
18
  * but is **not** bundled by `create_testing_actions` — it must enumerate
16
19
  * *every* mounted method, so it's appended at the full-mount layer
@@ -336,6 +339,47 @@ export declare const testing_schema_snapshot_action_spec: {
336
339
  * `create_testing_actions`; mount directly for in-process use.
337
340
  */
338
341
  export declare const create_testing_schema_snapshot_action: () => RpcAction;
342
+ /**
343
+ * `_testing_migration_tracker` — dump the `schema_version` tracker rows as a
344
+ * normalized `MigrationTracker` (`[{namespace, name, sequence}]`) for
345
+ * cross-impl migration-identity diffing. The provenance half of
346
+ * `_testing_schema_snapshot`: where that captures the resulting *schema*
347
+ * (and excludes the tracker by design), this captures the tracker *itself*,
348
+ * so the cross-backend harness can assert the two spines record byte-identical
349
+ * migration identity. This closes the gap that let the cell/fact
350
+ * migration-name divergence reach the visiones cutover undetected
351
+ * (`name-divergence-at-N` — same schema, divergent recorded names).
352
+ *
353
+ * `auth` gates on the daemon-token credential, matching `_testing_reset`. The
354
+ * Rust mirror is `fuz_testing::create_testing_migration_tracker_action_spec`.
355
+ */
356
+ export declare const testing_migration_tracker_action_spec: {
357
+ readonly method: "_testing_migration_tracker";
358
+ readonly kind: "request_response";
359
+ readonly initiator: "frontend";
360
+ readonly auth: {
361
+ readonly account: "required";
362
+ readonly actor: "none";
363
+ readonly credential_types: readonly ["daemon_token"];
364
+ };
365
+ readonly side_effects: false;
366
+ readonly input: z.ZodObject<{}, z.core.$strict>;
367
+ readonly output: z.ZodObject<{
368
+ entries: z.ZodArray<z.ZodObject<{
369
+ namespace: z.ZodString;
370
+ name: z.ZodString;
371
+ sequence: z.ZodNumber;
372
+ }, z.core.$strip>>;
373
+ }, z.core.$strip>;
374
+ readonly async: true;
375
+ readonly description: string;
376
+ };
377
+ /**
378
+ * Build the standalone `_testing_migration_tracker` action. No deps — reads
379
+ * `ctx.db`'s `schema_version` via `query_migration_tracker`. Bundled by
380
+ * `create_testing_actions`; mount directly for in-process use.
381
+ */
382
+ export declare const create_testing_migration_tracker_action: () => RpcAction;
339
383
  /**
340
384
  * `_testing_action_manifest` — dump the backend's live RPC method set as a
341
385
  * normalized `ActionManifest` (one entry per method: `{method, side_effects,
@@ -1 +1 @@
1
- {"version":3,"file":"testing_reset_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_reset_actions.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAItB,OAAO,EAAa,KAAK,SAAS,EAAC,MAAM,6BAA6B,CAAC;AAKvE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,gBAAgB,CAAC;AAgCvC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;QAiBpC;;;;;;;;WAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWyC,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;CAWA,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;CAwBC,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,mCAAmC,QAAO,SACiB,CAAC;AAEzE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;CAeK,CAAC;AAE/C;;;;;;;;;GASG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWF,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,QAAO,SAGvD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAYF,CAAC;AAyB/C,eAAO,MAAM,qCAAqC,GACjD,SAAS,aAAa,CAAC,SAAS,CAAC,KAC/B,SAMF,CAAC;AAEF,4CAA4C;AAC5C,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,SAAS,2BAA2B,KAClC,KAAK,CAAC,SAAS,CAqIjB,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,0BAA0B,UAAmC,CAAC"}
1
+ {"version":3,"file":"testing_reset_actions.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/testing_reset_actions.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoEG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAItB,OAAO,EAAa,KAAK,SAAS,EAAC,MAAM,6BAA6B,CAAC;AAKvE,OAAO,KAAK,EAAC,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAChD,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AACjE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,4BAA4B,CAAC;AACjE,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,gBAAgB,CAAC;AAqCvC;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;QAiBpC;;;;;;;;WAQG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWyC,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,iCAAiC;;;;;;;;;;;;;;;;CAWA,CAAC;AAE/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,gCAAgC;;;;;;;;;;;;;;;;;;;CAwBC,CAAC;AAE/C;;;;;;GAMG;AACH,eAAO,MAAM,mCAAmC,QAAO,SACiB,CAAC;AAEzE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,4BAA4B;;;;;;;;;;;;;;;;;;;CAeK,CAAC;AAE/C;;;;;;;;;GASG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAWF,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,qCAAqC,QAAO,SAGvD,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,qCAAqC;;;;;;;;;;;;;;;;;;;;CAYJ,CAAC;AAE/C;;;;GAIG;AACH,eAAO,MAAM,uCAAuC,QAAO,SAGzD,CAAC;AAEH;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,mCAAmC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAYF,CAAC;AAyB/C,eAAO,MAAM,qCAAqC,GACjD,SAAS,aAAa,CAAC,SAAS,CAAC,KAC/B,SAMF,CAAC;AAEF,4CAA4C;AAC5C,MAAM,WAAW,2BAA2B;IAC3C;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,EAAE,cAAc,CAAC,MAAM,CAAC,CAAC;IACjD;;;;;OAKG;IACH,QAAQ,CAAC,kBAAkB,EAAE,gBAAgB,CAAC;IAC9C;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,OAAO,EACb,SAAS,2BAA2B,KAClC,KAAK,CAAC,SAAS,CAsIjB,CAAC;AAEF,0FAA0F;AAC1F,eAAO,MAAM,0BAA0B,UAAmC,CAAC"}
@@ -2,15 +2,18 @@ import '../assert_dev_env.js';
2
2
  /**
3
3
  * Test-binary RPC actions for cross-process integration tests.
4
4
  *
5
- * Five daemon-token-authed actions, bundled by `create_testing_actions`:
5
+ * Six daemon-token-authed actions, bundled by `create_testing_actions`:
6
6
  * **`_testing_reset`** (DB wipe + keeper re-seed), **`_testing_drain_effects`**
7
7
  * (audit barrier), **`_testing_mint_session`** (forge an
8
8
  * expired-by-construction server-side session for the expiry conformance
9
9
  * cases), **`_testing_put_fact`** (seed an embedded fact for the fact-serving
10
- * suite), and **`_testing_schema_snapshot`** (introspect the live schema for
11
- * cross-impl parity diffing against a Rust backend's `fuz_db` snapshot).
10
+ * suite), **`_testing_schema_snapshot`** (introspect the live schema for
11
+ * cross-impl parity diffing against a Rust backend's `fuz_db` snapshot), and
12
+ * **`_testing_migration_tracker`** (dump the `schema_version` tracker rows for
13
+ * cross-impl migration-identity parity — the provenance half the snapshot gate
14
+ * excludes by design).
12
15
  *
13
- * A sixth daemon-token action, **`_testing_action_manifest`** (dump the live
16
+ * A further daemon-token action, **`_testing_action_manifest`** (dump the live
14
17
  * RPC registry for cross-impl method-set + auth-shape parity), lives here too
15
18
  * but is **not** bundled by `create_testing_actions` — it must enumerate
16
19
  * *every* mounted method, so it's appended at the full-mount layer
@@ -73,7 +76,7 @@ import { protocol_action_specs } from '../../actions/protocol.js';
73
76
  import { query_put_fact } from '../../db/fact_queries.js';
74
77
  import { ROLE_ADMIN, ROLE_KEEPER } from '../../auth/role_schema.js';
75
78
  import { auth_integration_truncate_tables } from '../db.js';
76
- import { query_schema_snapshot, SchemaSnapshot } from '../schema_introspect.js';
79
+ import { query_schema_snapshot, SchemaSnapshot, query_migration_tracker, MigrationTracker, } from '../schema_introspect.js';
77
80
  import { ActionManifest, build_action_manifest } from './action_manifest.js';
78
81
  import { query_create_actor } from '../../auth/account_queries.js';
79
82
  import { create_test_account_with_credentials, mint_test_session } from '../app_server.js';
@@ -302,6 +305,38 @@ export const testing_schema_snapshot_action_spec = {
302
305
  * `create_testing_actions`; mount directly for in-process use.
303
306
  */
304
307
  export const create_testing_schema_snapshot_action = () => rpc_action(testing_schema_snapshot_action_spec, async (input, ctx) => query_schema_snapshot(ctx.db, { exclude_tables: input.exclude_tables }));
308
+ /**
309
+ * `_testing_migration_tracker` — dump the `schema_version` tracker rows as a
310
+ * normalized `MigrationTracker` (`[{namespace, name, sequence}]`) for
311
+ * cross-impl migration-identity diffing. The provenance half of
312
+ * `_testing_schema_snapshot`: where that captures the resulting *schema*
313
+ * (and excludes the tracker by design), this captures the tracker *itself*,
314
+ * so the cross-backend harness can assert the two spines record byte-identical
315
+ * migration identity. This closes the gap that let the cell/fact
316
+ * migration-name divergence reach the visiones cutover undetected
317
+ * (`name-divergence-at-N` — same schema, divergent recorded names).
318
+ *
319
+ * `auth` gates on the daemon-token credential, matching `_testing_reset`. The
320
+ * Rust mirror is `fuz_testing::create_testing_migration_tracker_action_spec`.
321
+ */
322
+ export const testing_migration_tracker_action_spec = {
323
+ method: '_testing_migration_tracker',
324
+ kind: 'request_response',
325
+ initiator: 'frontend',
326
+ auth: TESTING_ACTION_AUTH,
327
+ side_effects: false,
328
+ input: z.strictObject({}),
329
+ output: MigrationTracker,
330
+ async: true,
331
+ description: 'Test-binary only — dump the schema_version tracker rows as a normalized {namespace, name, ' +
332
+ 'sequence} list for cross-impl migration-identity parity diffing.',
333
+ };
334
+ /**
335
+ * Build the standalone `_testing_migration_tracker` action. No deps — reads
336
+ * `ctx.db`'s `schema_version` via `query_migration_tracker`. Bundled by
337
+ * `create_testing_actions`; mount directly for in-process use.
338
+ */
339
+ export const create_testing_migration_tracker_action = () => rpc_action(testing_migration_tracker_action_spec, async (_input, ctx) => query_migration_tracker(ctx.db));
305
340
  /**
306
341
  * `_testing_action_manifest` — dump the backend's live RPC method set as a
307
342
  * normalized `ActionManifest` (one entry per method: `{method, side_effects,
@@ -495,6 +530,7 @@ export const create_testing_actions = (deps, options) => {
495
530
  }),
496
531
  create_testing_drain_effects_action(),
497
532
  create_testing_schema_snapshot_action(),
533
+ create_testing_migration_tracker_action(),
498
534
  ];
499
535
  };
500
536
  /** Set of auth-namespace tables `_testing_reset` wipes. Mirrored by the coverage test. */
@@ -109,6 +109,45 @@ export declare const SchemaSnapshot: z.ZodObject<{
109
109
  }, z.core.$strip>>;
110
110
  }, z.core.$strip>;
111
111
  export type SchemaSnapshot = z.infer<typeof SchemaSnapshot>;
112
+ /**
113
+ * A single `schema_version` tracker row — the migration-identity primitive.
114
+ *
115
+ * Where `SchemaSnapshot` is provenance-agnostic (it captures the resulting
116
+ * tables and deliberately *excludes* the tracker), this **is** the tracker:
117
+ * the `(namespace, name, sequence)` the migration runner records per applied
118
+ * migration. `sequence` carries order; `name` carries identity (the PK is
119
+ * `(namespace, name)`). The cross-impl gate diffs these between the two
120
+ * bootstrapped spines so a migration-name or partitioning drift — invisible
121
+ * to the schema snapshot — is a gated fact, not a latent interop break.
122
+ */
123
+ export declare const MigrationTrackerEntry: z.ZodObject<{
124
+ namespace: z.ZodString;
125
+ name: z.ZodString;
126
+ sequence: z.ZodNumber;
127
+ }, z.core.$strip>;
128
+ export type MigrationTrackerEntry = z.infer<typeof MigrationTrackerEntry>;
129
+ /**
130
+ * The full `schema_version` tracker as a deterministically-ordered list,
131
+ * wrapped in an object so it round-trips cleanly as a JSON-RPC result.
132
+ * Sorted by `(namespace, sequence)` on capture.
133
+ */
134
+ export declare const MigrationTracker: z.ZodObject<{
135
+ entries: z.ZodArray<z.ZodObject<{
136
+ namespace: z.ZodString;
137
+ name: z.ZodString;
138
+ sequence: z.ZodNumber;
139
+ }, z.core.$strip>>;
140
+ }, z.core.$strip>;
141
+ export type MigrationTracker = z.infer<typeof MigrationTracker>;
142
+ /**
143
+ * Read every `schema_version` row into a deterministic `MigrationTracker`.
144
+ *
145
+ * The migration-identity twin of `query_schema_snapshot`: that captures the
146
+ * resulting schema (and excludes this tracker); this captures the tracker
147
+ * rows themselves, so the cross-impl gate can assert the two spines record
148
+ * byte-identical migration identity.
149
+ */
150
+ export declare const query_migration_tracker: (db: Db) => Promise<MigrationTracker>;
112
151
  /** Filter options for `query_schema_snapshot`. */
113
152
  export interface QuerySchemaSnapshotOptions {
114
153
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"schema_introspect.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_introspect.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAEpC;;;;;GAKG;AACH,eAAO,MAAM,cAAc;;;;;;iBAWzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,qCAAqC;AACrC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;iBAOxB,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,qFAAqF;AACrF,eAAO,MAAM,gBAAgB;;iBAE3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB;;iBAG3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;iBAOzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,kDAAkD;AAClD,MAAM,WAAW,0BAA0B;IAC1C;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChD;AA8DD;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GACjC,IAAI,EAAE,EACN,UAAS,0BAA+B,KACtC,OAAO,CAAC,cAAc,CAyHxB,CAAC"}
1
+ {"version":3,"file":"schema_introspect.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_introspect.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,aAAa,CAAC;AAEpC;;;;;GAKG;AACH,eAAO,MAAM,cAAc;;;;;;iBAWzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D,qCAAqC;AACrC,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;iBAOxB,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D,qFAAqF;AACrF,eAAO,MAAM,gBAAgB;;iBAE3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB;;iBAG3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;iBAOzB,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAE5D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB;;;;iBAOhC,CAAC;AACH,MAAM,MAAM,qBAAqB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAE1E;;;;GAIG;AACH,eAAO,MAAM,gBAAgB;;;;;;iBAE3B,CAAC;AACH,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAEhE;;;;;;;GAOG;AACH,eAAO,MAAM,uBAAuB,GAAU,IAAI,EAAE,KAAG,OAAO,CAAC,gBAAgB,CAa9E,CAAC;AAEF,kDAAkD;AAClD,MAAM,WAAW,0BAA0B;IAC1C;;;OAGG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAChD;AA8DD;;;;;;;;;GASG;AACH,eAAO,MAAM,qBAAqB,GACjC,IAAI,EAAE,EACN,UAAS,0BAA+B,KACtC,OAAO,CAAC,cAAc,CAyHxB,CAAC"}
@@ -81,6 +81,53 @@ export const SchemaSnapshot = z.object({
81
81
  /** Enum types (`CREATE TYPE ... AS ENUM`) keyed by name; labels in declared order. */
82
82
  enums: z.record(z.string(), EnumTypeSnapshot),
83
83
  });
84
+ /**
85
+ * A single `schema_version` tracker row — the migration-identity primitive.
86
+ *
87
+ * Where `SchemaSnapshot` is provenance-agnostic (it captures the resulting
88
+ * tables and deliberately *excludes* the tracker), this **is** the tracker:
89
+ * the `(namespace, name, sequence)` the migration runner records per applied
90
+ * migration. `sequence` carries order; `name` carries identity (the PK is
91
+ * `(namespace, name)`). The cross-impl gate diffs these between the two
92
+ * bootstrapped spines so a migration-name or partitioning drift — invisible
93
+ * to the schema snapshot — is a gated fact, not a latent interop break.
94
+ */
95
+ export const MigrationTrackerEntry = z.object({
96
+ /** Migration namespace (e.g. `fuz_cell`, `fuz_cell_history`). */
97
+ namespace: z.string(),
98
+ /** Migration name within the namespace (e.g. `full_cell_schema`). */
99
+ name: z.string(),
100
+ /** Per-namespace apply order, starting at 0. */
101
+ sequence: z.number().int(),
102
+ });
103
+ /**
104
+ * The full `schema_version` tracker as a deterministically-ordered list,
105
+ * wrapped in an object so it round-trips cleanly as a JSON-RPC result.
106
+ * Sorted by `(namespace, sequence)` on capture.
107
+ */
108
+ export const MigrationTracker = z.object({
109
+ entries: z.array(MigrationTrackerEntry),
110
+ });
111
+ /**
112
+ * Read every `schema_version` row into a deterministic `MigrationTracker`.
113
+ *
114
+ * The migration-identity twin of `query_schema_snapshot`: that captures the
115
+ * resulting schema (and excludes this tracker); this captures the tracker
116
+ * rows themselves, so the cross-impl gate can assert the two spines record
117
+ * byte-identical migration identity.
118
+ */
119
+ export const query_migration_tracker = async (db) => {
120
+ const rows = await db.query(`SELECT namespace, name, sequence
121
+ FROM schema_version
122
+ ORDER BY namespace ASC, sequence ASC`);
123
+ return {
124
+ entries: rows.map((r) => ({
125
+ namespace: r.namespace,
126
+ name: r.name,
127
+ sequence: r.sequence,
128
+ })),
129
+ };
130
+ };
84
131
  const sort_keys = (record) => {
85
132
  const sorted = {};
86
133
  for (const key of Object.keys(record).sort()) {
@@ -39,7 +39,7 @@ import './assert_dev_env.js';
39
39
  *
40
40
  * @module
41
41
  */
42
- import type { ColumnSnapshot, SchemaSnapshot } from './schema_introspect.js';
42
+ import type { ColumnSnapshot, MigrationTracker, SchemaSnapshot } from './schema_introspect.js';
43
43
  /** Structured drift entry. `where` is the named source impl ('a' or 'b'). */
44
44
  export type SchemaDiff = {
45
45
  readonly kind: 'table_only_in';
@@ -139,4 +139,45 @@ export declare const format_schema_diffs: (diffs: ReadonlyArray<SchemaDiff>, lab
139
139
  * ```
140
140
  */
141
141
  export declare const assert_schema_snapshots_equal: (a: SchemaSnapshot, b: SchemaSnapshot, labels?: SchemaDiffLabels) => void;
142
+ /**
143
+ * Structured migration-identity drift entry. Keyed on `(namespace, name)` —
144
+ * the `schema_version` PK — so a name rename and a partitioning change both
145
+ * surface as `tracker_row_only_in`, and a re-order surfaces as
146
+ * `tracker_sequence_differs`.
147
+ */
148
+ export type MigrationTrackerDiff = {
149
+ readonly kind: 'tracker_row_only_in';
150
+ readonly where: 'a' | 'b';
151
+ readonly namespace: string;
152
+ readonly name: string;
153
+ } | {
154
+ readonly kind: 'tracker_sequence_differs';
155
+ readonly namespace: string;
156
+ readonly name: string;
157
+ readonly a: number;
158
+ readonly b: number;
159
+ };
160
+ /**
161
+ * Structural diff between two migration trackers — empty array means the two
162
+ * spines recorded byte-identical migration identity. Keyed on
163
+ * `(namespace, name)`; sequence mismatches on a shared key are reported too.
164
+ * Deterministic order: shared/missing rows in sorted `(namespace, name)` order.
165
+ */
166
+ export declare const diff_migration_trackers: (a: MigrationTracker, b: MigrationTracker) => Array<MigrationTrackerDiff>;
167
+ /**
168
+ * Render migration-tracker diffs as a human-readable multi-line string.
169
+ * Empty diffs produce an empty string.
170
+ */
171
+ export declare const format_migration_tracker_diffs: (diffs: ReadonlyArray<MigrationTrackerDiff>, labels?: SchemaDiffLabels) => string;
172
+ /**
173
+ * Throw if the two spines' `schema_version` trackers disagree — the gate for
174
+ * the swap-freely invariant (any consumer can swap TS↔Rust over one DB without
175
+ * re-bootstrapping). This catches what `assert_schema_snapshots_equal` is blind
176
+ * to by design: the snapshot excludes the tracker, so a migration-name or
177
+ * partitioning divergence that yields an identical *schema* (e.g. `cell_v0` vs
178
+ * `full_cell_schema`, or `cell_history` bundled vs isolated) passes schema
179
+ * parity but breaks the runner's positional name-prefix check at boot
180
+ * (`name-divergence-at-N`). The error names the impls and lists every diff.
181
+ */
182
+ export declare const assert_migration_trackers_equal: (a: MigrationTracker, b: MigrationTracker, labels?: SchemaDiffLabels) => void;
142
183
  //# sourceMappingURL=schema_parity.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema_parity.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_parity.ts"],"names":[],"mappings":"AAAA,OAAO,qBAAqB,CAAC;AAE7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,OAAO,KAAK,EACX,cAAc,EAEd,cAAc,EAGd,MAAM,wBAAwB,CAAC;AAEhC,6EAA6E;AAC7E,MAAM,MAAM,UAAU,GACnB;IAAC,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAC,GACnF;IACA,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAChC,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACvB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,sBAAsB,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,cAAc,CAAC;IACrC,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;CACnB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACtB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,0BAA0B,CAAC;IAC1C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CAClB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC;IAC1B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC3B,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAC,CAAC;IAC/C,QAAQ,CAAC,CAAC,EAAE;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAC,CAAC;CAC9C,GACD;IAAC,QAAQ,CAAC,IAAI,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,GACzF;IACA,QAAQ,CAAC,IAAI,EAAE,4BAA4B,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CAClB,GACD;IAAC,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAC,GACtF;IACA,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAClC,QAAQ,CAAC,CAAC,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CACjC,CAAC;AAEL;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GAAI,GAAG,cAAc,EAAE,GAAG,cAAc,KAAG,KAAK,CAAC,UAAU,CAiD5F,CAAC;AAqHF,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAChC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,mBAAmB,GAC/B,OAAO,aAAa,CAAC,UAAU,CAAC,EAChC,SAAQ,gBAAqB,KAC3B,MAoEF,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,6BAA6B,GACzC,GAAG,cAAc,EACjB,GAAG,cAAc,EACjB,SAAQ,gBAAqB,KAC3B,IAUF,CAAC"}
1
+ {"version":3,"file":"schema_parity.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/testing/schema_parity.ts"],"names":[],"mappings":"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"}
@@ -225,3 +225,94 @@ export const assert_schema_snapshots_equal = (a, b, labels = {}) => {
225
225
  const label_b = labels.b ?? 'b';
226
226
  throw new Error(`Schema parity failed: ${diffs.length} diff(s) between ${label_a} and ${label_b}\n${format_schema_diffs(diffs, labels)}`);
227
227
  };
228
+ const tracker_key = (namespace, name) => `${namespace}${name}`;
229
+ /**
230
+ * Structural diff between two migration trackers — empty array means the two
231
+ * spines recorded byte-identical migration identity. Keyed on
232
+ * `(namespace, name)`; sequence mismatches on a shared key are reported too.
233
+ * Deterministic order: shared/missing rows in sorted `(namespace, name)` order.
234
+ */
235
+ export const diff_migration_trackers = (a, b) => {
236
+ const diffs = [];
237
+ const a_by_key = new Map(a.entries.map((e) => [tracker_key(e.namespace, e.name), e]));
238
+ const b_by_key = new Map(b.entries.map((e) => [tracker_key(e.namespace, e.name), e]));
239
+ // Union keyed by `(namespace, name)`; the value is a reference entry (from
240
+ // whichever side has it) used to recover namespace/name for the diff — so a
241
+ // row present on only one side needs no non-null assertion.
242
+ const by_key = new Map([...a_by_key, ...b_by_key]);
243
+ for (const [key, ref] of [...by_key].sort((x, y) => (x[0] < y[0] ? -1 : x[0] > y[0] ? 1 : 0))) {
244
+ const ea = a_by_key.get(key);
245
+ const eb = b_by_key.get(key);
246
+ if (!ea) {
247
+ diffs.push({
248
+ kind: 'tracker_row_only_in',
249
+ where: 'b',
250
+ namespace: ref.namespace,
251
+ name: ref.name,
252
+ });
253
+ continue;
254
+ }
255
+ if (!eb) {
256
+ diffs.push({
257
+ kind: 'tracker_row_only_in',
258
+ where: 'a',
259
+ namespace: ref.namespace,
260
+ name: ref.name,
261
+ });
262
+ continue;
263
+ }
264
+ if (ea.sequence !== eb.sequence) {
265
+ diffs.push({
266
+ kind: 'tracker_sequence_differs',
267
+ namespace: ea.namespace,
268
+ name: ea.name,
269
+ a: ea.sequence,
270
+ b: eb.sequence,
271
+ });
272
+ }
273
+ }
274
+ return diffs;
275
+ };
276
+ /**
277
+ * Render migration-tracker diffs as a human-readable multi-line string.
278
+ * Empty diffs produce an empty string.
279
+ */
280
+ export const format_migration_tracker_diffs = (diffs, labels = {}) => {
281
+ if (diffs.length === 0)
282
+ return '';
283
+ const label_a = labels.a ?? 'a';
284
+ const label_b = labels.b ?? 'b';
285
+ const lines = [];
286
+ for (const d of diffs) {
287
+ switch (d.kind) {
288
+ case 'tracker_row_only_in':
289
+ lines.push(` migration ${d.namespace}/${d.name} only in ${d.where === 'a' ? label_a : label_b}`);
290
+ break;
291
+ case 'tracker_sequence_differs':
292
+ lines.push(` migration ${d.namespace}/${d.name} sequence differs: ${label_a}=${d.a}, ${label_b}=${d.b}`);
293
+ break;
294
+ default:
295
+ d;
296
+ break;
297
+ }
298
+ }
299
+ return lines.join('\n');
300
+ };
301
+ /**
302
+ * Throw if the two spines' `schema_version` trackers disagree — the gate for
303
+ * the swap-freely invariant (any consumer can swap TS↔Rust over one DB without
304
+ * re-bootstrapping). This catches what `assert_schema_snapshots_equal` is blind
305
+ * to by design: the snapshot excludes the tracker, so a migration-name or
306
+ * partitioning divergence that yields an identical *schema* (e.g. `cell_v0` vs
307
+ * `full_cell_schema`, or `cell_history` bundled vs isolated) passes schema
308
+ * parity but breaks the runner's positional name-prefix check at boot
309
+ * (`name-divergence-at-N`). The error names the impls and lists every diff.
310
+ */
311
+ export const assert_migration_trackers_equal = (a, b, labels = {}) => {
312
+ const diffs = diff_migration_trackers(a, b);
313
+ if (diffs.length === 0)
314
+ return;
315
+ const label_a = labels.a ?? 'a';
316
+ const label_b = labels.b ?? 'b';
317
+ throw new Error(`Migration-identity parity failed: ${diffs.length} diff(s) between ${label_a} and ${label_b}\n${format_migration_tracker_diffs(diffs, labels)}`);
318
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fuzdev/fuz_app",
3
- "version": "0.98.1",
3
+ "version": "0.100.0",
4
4
  "description": "fullstack app library",
5
5
  "glyph": "🗝",
6
6
  "logo": "logo.svg",