@fuzdev/fuz_app 0.90.0 → 0.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/actions/action_rpc.d.ts.map +1 -1
  2. package/dist/actions/action_rpc.js +2 -2
  3. package/dist/actions/perform_action.d.ts.map +1 -1
  4. package/dist/actions/perform_action.js +6 -6
  5. package/dist/actions/transports_ws.d.ts.map +1 -1
  6. package/dist/actions/transports_ws.js +2 -1
  7. package/dist/actions/transports_ws_backend.d.ts.map +1 -1
  8. package/dist/actions/transports_ws_backend.js +2 -1
  9. package/dist/auth/CLAUDE.md +7 -0
  10. package/dist/auth/all_cell_actions.d.ts +35 -0
  11. package/dist/auth/all_cell_actions.d.ts.map +1 -0
  12. package/dist/auth/all_cell_actions.js +32 -0
  13. package/dist/auth/bootstrap_routes.d.ts.map +1 -1
  14. package/dist/auth/bootstrap_routes.js +2 -1
  15. package/dist/auth/cell_actions.d.ts.map +1 -1
  16. package/dist/auth/cell_actions.js +2 -4
  17. package/dist/db/fact_disk_storage.d.ts.map +1 -1
  18. package/dist/db/fact_disk_storage.js +2 -1
  19. package/dist/db/fact_store.d.ts.map +1 -1
  20. package/dist/db/fact_store.js +2 -1
  21. package/dist/db/migrate.d.ts.map +1 -1
  22. package/dist/db/migrate.js +2 -1
  23. package/dist/db/status.d.ts.map +1 -1
  24. package/dist/db/status.js +2 -1
  25. package/dist/http/error_schemas.d.ts +5 -3
  26. package/dist/http/error_schemas.d.ts.map +1 -1
  27. package/dist/http/error_schemas.js +5 -3
  28. package/dist/http/jsonrpc_errors.d.ts +21 -0
  29. package/dist/http/jsonrpc_errors.d.ts.map +1 -1
  30. package/dist/http/jsonrpc_errors.js +22 -0
  31. package/dist/http/route_spec.d.ts.map +1 -1
  32. package/dist/http/route_spec.js +11 -8
  33. package/dist/runtime/deno.d.ts.map +1 -1
  34. package/dist/runtime/deno.js +2 -1
  35. package/dist/server/env.d.ts.map +1 -1
  36. package/dist/server/env.js +2 -1
  37. package/dist/testing/CLAUDE.md +82 -9
  38. package/dist/testing/cross_backend/action_manifest.d.ts +113 -0
  39. package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -0
  40. package/dist/testing/cross_backend/action_manifest.js +89 -0
  41. package/dist/testing/cross_backend/action_manifest_parity.d.ts +67 -0
  42. package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -0
  43. package/dist/testing/cross_backend/action_manifest_parity.js +88 -0
  44. package/dist/testing/cross_backend/capabilities.d.ts +47 -30
  45. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  46. package/dist/testing/cross_backend/capabilities.js +19 -4
  47. package/dist/testing/cross_backend/default_backend_configs.d.ts +31 -15
  48. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  49. package/dist/testing/cross_backend/default_backend_configs.js +30 -14
  50. package/dist/testing/cross_backend/full_spine_mount.d.ts +81 -0
  51. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -0
  52. package/dist/testing/cross_backend/full_spine_mount.js +49 -0
  53. package/dist/testing/cross_backend/method_coverage.d.ts +71 -0
  54. package/dist/testing/cross_backend/method_coverage.d.ts.map +1 -0
  55. package/dist/testing/cross_backend/method_coverage.js +89 -0
  56. package/dist/testing/cross_backend/setup.d.ts +12 -0
  57. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  58. package/dist/testing/cross_backend/setup.js +19 -0
  59. package/dist/testing/cross_backend/testing_backdoor.d.ts.map +1 -1
  60. package/dist/testing/cross_backend/testing_backdoor.js +9 -5
  61. package/dist/testing/cross_backend/testing_reset_actions.d.ts +76 -2
  62. package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
  63. package/dist/testing/cross_backend/testing_reset_actions.js +63 -2
  64. package/dist/testing/db.d.ts.map +1 -1
  65. package/dist/testing/db.js +2 -1
  66. package/dist/ui/AdminSurface.svelte +3 -1
  67. package/dist/ui/AdminSurface.svelte.d.ts.map +1 -1
  68. package/dist/ui/async_slot.svelte.d.ts.map +1 -1
  69. package/dist/ui/async_slot.svelte.js +2 -1
  70. package/dist/ui/auth_state.svelte.d.ts.map +1 -1
  71. package/dist/ui/auth_state.svelte.js +4 -3
  72. package/dist/ui/table_state.svelte.d.ts.map +1 -1
  73. package/dist/ui/table_state.svelte.js +2 -1
  74. package/package.json +4 -4
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * @module
9
9
  */
10
+ import { to_error_message } from '@fuzdev/fuz_util/error.js';
10
11
  /**
11
12
  * Create a runtime backed by Deno APIs.
12
13
  *
@@ -135,7 +136,7 @@ export const create_deno_runtime = (args) => ({
135
136
  }
136
137
  }
137
138
  catch (error) {
138
- const message = error instanceof Error ? error.message : String(error);
139
+ const message = to_error_message(error);
139
140
  return {
140
141
  success: false,
141
142
  code: 1,
@@ -1 +1 @@
1
- {"version":3,"file":"env.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,EAA2B,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAG1E;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;kBAoDxB,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,wBAAwB,GAAG,qBAAqB,CAAC;IACxD,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,qBAAqB,CAAC;AAE9E;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,KAAK,aAAa,KAAG,sBA4BxD,CAAC"}
1
+ {"version":3,"file":"env.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/server/env.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,OAAO,EAA2B,KAAK,OAAO,EAAC,MAAM,oBAAoB,CAAC;AAG1E;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;kBAoDxB,CAAC;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC;AAE1D;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,IAAI,CAAC;IACT,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC/B,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;CACpC;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACrC,EAAE,EAAE,KAAK,CAAC;IACV,KAAK,EAAE,wBAAwB,GAAG,qBAAqB,CAAC;IACxD,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACtB;AAED,MAAM,MAAM,sBAAsB,GAAG,gBAAgB,GAAG,qBAAqB,CAAC;AAE9E;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAAI,KAAK,aAAa,KAAG,sBA4BxD,CAAC"}
@@ -9,6 +9,7 @@
9
9
  * @module
10
10
  */
11
11
  import { z } from 'zod';
12
+ import { to_error_message } from '@fuzdev/fuz_util/error.js';
12
13
  import { create_validated_keyring } from '../auth/keyring.js';
13
14
  import { parse_allowed_origins } from '../http/origin.js';
14
15
  /**
@@ -94,7 +95,7 @@ export const validate_server_env = (env) => {
94
95
  return {
95
96
  ok: false,
96
97
  field: 'FUZ_ALLOWED_ORIGINS',
97
- errors: [err instanceof Error ? err.message : 'Invalid FUZ_ALLOWED_ORIGINS'],
98
+ errors: [to_error_message(err, 'Invalid FUZ_ALLOWED_ORIGINS')],
98
99
  };
99
100
  }
100
101
  if (allowed_origins.length === 0) {
@@ -222,7 +222,37 @@ test-only by construction.
222
222
  - `assert_schema_snapshots_equal(a, b, labels?)` — throws on drift with a fully-formatted diff message.
223
223
  - `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).
224
224
 
225
- fuz_app's own spine gates this cross-process via the `cross_backend_schema_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:schema-parity`. The forge has its own deno↔rust parity gate.
225
+ 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
+
227
+ ## Cross-impl RPC-manifest parity
228
+
229
+ The action-surface sibling of schema parity. Where schema parity diffs the live
230
+ _database_ shape, this diffs the live _RPC registry_ shape — proving the TS
231
+ spine and the Rust `testing_spine_stub` mount the same method set with the same
232
+ per-method auth shape.
233
+
234
+ - `cross_backend/action_manifest.ts` — `ActionManifest` / `ActionManifestEntry`
235
+ Zod schema (the `_testing_action_manifest` wire validator) + `build_action_manifest(specs)`,
236
+ which normalizes each spec to `{method, side_effects, account, actor, roles,
237
+ credential_types}` (auth axes pass through; `roles` / `credential_types` flatten
238
+ to sorted arrays). Twin of `schema_introspect.ts`. Excludes the protocol actions
239
+ (`heartbeat` / `cancel`) by construction — the two impls organize them
240
+ differently (TS WS-only; Rust on one shared registry), so the caller passes a
241
+ protocol-free list.
242
+ - `cross_backend/action_manifest_parity.ts` — `diff_action_manifests` /
243
+ `format_action_manifest_diffs` / `assert_action_manifests_equal` (twin of
244
+ `schema_parity.ts`) — asserts exact method-set + per-method auth-shape parity.
245
+ - `cross_backend/setup.ts` `capture_action_manifest(handle)` — dumps a backend's
246
+ manifest over the daemon-token `_testing_action_manifest` channel (twin of
247
+ `capture_schema_snapshot`).
248
+
249
+ It runs under the same dual-spawn `cross_backend_parity` project (one TS-spine +
250
+ Rust-stub spawn serves both gates). It complements the in-repo
251
+ `spine_method_coverage` reconciliation gate (which proves _mounted ⟹ covered_);
252
+ this proves _TS-mount-set ≡ Rust-mount-set_. The gate asserts **exact** parity —
253
+ every mounted method and its full auth shape (side-effects + the four auth axes)
254
+ must match across the two impls, with no allowlisted divergences (see
255
+ `action_manifest_parity.cross.test.ts`).
226
256
 
227
257
  **Cross-impl gate pattern** — a dual-impl consumer running two backends
228
258
  (a TS Hono server and a Rust spine server) against a shared schema, plus
@@ -861,18 +891,26 @@ source of truth for wire-shape conformance.
861
891
  deliver it.
862
892
 
863
893
  - `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
864
- (`bearer_auth` / `trusted_proxy` / `login_rate_limit` / `ws` / `sse` /
865
- `cell_crud` / `cell_relations` / `account_lifecycle` / `fact_serving` /
866
- `ready` / `account_status` / `oversized_reject_closes_connection`),
894
+ (the **gating** flags `ws` / `sse` / `cell_crud` / `cell_relations` /
895
+ `account_lifecycle` / `fact_serving` / `ready` / `account_status` /
896
+ `oversized_reject_closes_connection` — each has a `test_if` reader),
867
897
  `test_if(cond, name, fn)`
868
- for capability-gated cases, and `in_process_capabilities` preset. `cell_crud`
898
+ for capability-gated cases, and `in_process_capabilities` preset. The
899
+ non-gating wiring facts (`bearer_auth` / `trusted_proxy` / `login_rate_limit`,
900
+ no `test_if` readers) live in the parallel `BackendShapeNotes` record
901
+ (`in_process_shape_notes`; `ts_default_shape_notes` / `rust_default_shape_notes`
902
+ in `default_backend_configs.ts`) — split out so the capability type stops
903
+ claiming gating it doesn't do. `cell_crud`
869
904
  gates the CRUD parity suite, `cell_relations` the relation / ACL / audit
870
905
  parity suite — both `true` on every backend that live-mounts the full cell
871
906
  surface (TS spine binary, in-process app, Rust stub). A backend mounting only
872
907
  plain CRUD would declare `cell_crud: true, cell_relations: false`.
873
908
  `account_lifecycle` gates `describe_account_lifecycle_cross_tests` (the
874
- `account_delete` / `account_undelete` / `account_purge` parity suite) — also
875
- off the declared surface like cells, `true` on every spine. `fact_serving`
909
+ `account_delete` / `account_undelete` / `account_purge` parity suite); unlike
910
+ cells those verbs **are** on the declared surface (in `create_admin_actions`,
911
+ auto-enumerated for shape/auth) — the flag gates the behavioral parity the
912
+ generic round-trip can't drive (it can't delete the subject). `true` on every
913
+ spine. `fact_serving`
876
914
  gates `describe_fact_serving_cross_tests` (the cell-scoped per-reference +
877
915
  admin-only bare-hash fact-serving parity suite); like cells it stays off the
878
916
  declared surface and is `true` on every spine that mounts the serve routes +
@@ -1191,6 +1229,16 @@ in-process legs (plain `gro test`) are `src/test/auth/cell_crud_parity.db.test.t
1191
1229
  payload-expiry tests never reached. Daemon-token-gated like its siblings; the
1192
1230
  Rust mirror is `fuz_testing::create_testing_mint_session_action_spec`.
1193
1231
 
1232
+ Also in the module — but **not** bundled by `create_testing_actions` —
1233
+ `_testing_action_manifest` (+ `create_testing_action_manifest_action(mounted)`):
1234
+ dumps the live RPC registry as a normalized `ActionManifest` for the
1235
+ cross-impl manifest-parity gate (see §Cross-impl RPC-manifest parity). It must
1236
+ enumerate _every_ mounted method, which `create_testing_actions` (one
1237
+ sub-factory) can't see, so it's appended at the full-mount layer
1238
+ (`full_spine_mount.ts` `build_full_spine_rpc_actions`), closing over the
1239
+ complete action list plus its own spec. Daemon-token-gated like its siblings;
1240
+ the Rust mirror is `fuz_testing::create_testing_action_manifest_action_spec`.
1241
+
1194
1242
  ### Origin verification parity — `cross_backend/origin.ts`
1195
1243
 
1196
1244
  `describe_origin_cross_tests({setup_test, capabilities, rpc_path?})` — the
@@ -1309,10 +1357,35 @@ re-roll the serve / daemon-info / WS-attach / drain boilerplate:
1309
1357
  - `testing/cross_backend/testing_server_node.ts` — `create_node_testing_adapter()` (`@hono/node-server` + `@hono/node-ws`). Optional peer deps (like `ws`); only test binaries import them.
1310
1358
  - `testing/cross_backend/testing_server_deno.ts` — `create_deno_testing_adapter()` (`Deno.serve` + `hono/deno`; `Deno` declared locally so it typechecks under the Node toolchain). Spawn the entry with `--sloppy-imports` (Deno doesn't do `.js`→`.ts`; Gro's loader does, so the Node path needs no flag).
1311
1359
  - `testing/cross_backend/testing_server_bun.ts` — `create_bun_testing_adapter()` (`Bun.serve` + `hono/bun`'s module-level `upgradeWebSocket` + `websocket`; `Bun.serve` declared locally so it typechecks under the Node toolchain). **No extra deps** (`hono/bun` ships with `hono`; `Bun.serve` is built in, unlike Node's `@hono/node-server` + `@hono/node-ws`), and Bun resolves `.js`→`.ts` natively (no flag, unlike Deno). Reuses `create_node_runtime` (Bun implements the `node:fs`/`node:process` surface). WS is module-level + stateless (like Deno) — the `websocket` handler is threaded into `serve`, where `Bun.serve` wants it, so no post-serve attach.
1312
- - `testing/cross_backend/default_spine_surface.ts` — the canonical no-domain spine surface (account/admin/audit/signup + bootstrap): `spine_session_options`, `spine_roles`, `create_spine_route_specs`, `spine_rpc_endpoints`, `create_spine_surface_spec`. `$lib`-free (it's reached by the spawned binary under Gro's loader, which doesn't resolve `$lib`), so keep it on relative imports. Shared by the spine_stub cross test, the TS cross tests, and the binary.
1360
+ - `testing/cross_backend/default_spine_surface.ts` — the canonical no-domain spine surface (account/admin/audit/signup + bootstrap): `spine_session_options`, `spine_roles`, `create_spine_route_specs`, `spine_rpc_endpoints`, `create_spine_surface_spec`. This is the **declared** surface — the `create_standard_rpc_actions` bundle the spec-derived suites auto-enumerate. `$lib`-free (it's reached by the spawned binary under Gro's loader, which doesn't resolve `$lib`), so keep it on relative imports. Shared by the spine_stub cross test, the TS cross tests, and the binary.
1361
+ - `testing/cross_backend/full_spine_mount.ts` — `build_full_spine_rpc_actions(deps, options)` / `full_spine_rpc_endpoints(ctx, options)` — the **full** live RPC mount: the declared bundle **plus** the off-declared-surface families the binary live-mounts (`_testing_*` backdoors, the cell verb set, the opt-in `actor_lookup` / `actor_search` resolvers). Single-sources what was an inline assembly in `testing_spine_server.ts`, so the binary and the `spine_method_coverage` reconciliation test build the same list. Also `$lib`-free.
1313
1362
  - `testing/cross_backend/ts_spine_backend_config.ts` — `ts_spine_node_backend_config()` / `ts_spine_deno_backend_config()` / `ts_spine_bun_backend_config()` presets (in-memory PGlite, no external infra), the TS analog of `rust_spine_stub_backend_config()`.
1314
1363
 
1315
- fuz_app's own binary wiring (`src/test/cross_backend/testing_spine_server{,_node,_deno,_bun}.ts`) is the worked example: ~one `build_app` over `create_app_backend` + `create_app_server` + `_testing_reset` + a WS mount, reusing `default_spine_surface`. The `_node`/`_deno`/`_bun` entries differ only in which adapter they wire — `build_spine_app` is runtime-agnostic.
1364
+ fuz_app's own binary wiring (`src/test/cross_backend/testing_spine_server{,_node,_deno,_bun}.ts`) is the worked example: ~one `build_app` over `create_app_backend` + `create_app_server` + `full_spine_rpc_endpoints` + a WS mount, reusing `default_spine_surface`. The `_node`/`_deno`/`_bun` entries differ only in which adapter they wire — `build_spine_app` is runtime-agnostic.
1365
+
1366
+ ### Live-method coverage reconciliation — `method_coverage.ts`
1367
+
1368
+ `assert_rpc_method_coverage({live_methods, declared_methods, manifest})` keeps
1369
+ the **off-declared-surface** methods as drift-proof as the declared ones. The
1370
+ spec-derived suites auto-enumerate the declared surface, but a backend's live
1371
+ RPC endpoint also mounts methods kept off it (stateful cell verbs, opt-in
1372
+ resolvers, `_testing_*` backdoors) that rely on hand-wired imperative suites —
1373
+ nothing structurally guaranteed every live method was actually claimed by one.
1374
+
1375
+ The assertion diffs the live method set against a `MethodCoverageEntry`
1376
+ manifest both directions (a mounted-but-unclaimed method **and** a stale
1377
+ manifest row both fail loud) and checks each entry's `tier`
1378
+ (`declared` / `off_surface` / `backdoor`) is consistent with the declared
1379
+ surface + the `_testing_` prefix. `off_surface` rows must name the covering
1380
+ `suite`; `capability` is typed `keyof BackendCapabilities` so a stale flag is a
1381
+ compile error. The manifest is the forcing function — a method can't reach the
1382
+ live mount without a row naming its coverage.
1383
+
1384
+ Pairs with `surface_invariants.ts` `assert_no_testing_methods` (the _reverse_
1385
+ guard — a backdoor must never leak _onto_ the declared surface). fuz_app's own
1386
+ wiring is the manifest `src/test/cross_backend/spine_method_coverage.ts` + the
1387
+ no-DB reconciliation test `spine_method_coverage.test.ts`, which enumerates
1388
+ `build_full_spine_rpc_actions` with stub deps against `create_spine_surface_spec`.
1316
1389
 
1317
1390
  The in-process `ws_round_trip` harness stays (it drives the dispatcher
1318
1391
  against a fake upgrade, no wire), but the real-upgrade coverage now lives in
@@ -0,0 +1,113 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-impl RPC **action-manifest** introspection — a normalized,
4
+ * JSON-serializable dump of a backend's live RPC method set, one entry per
5
+ * method carrying its auth shape + side-effect flag.
6
+ *
7
+ * The sibling of `schema_introspect.ts`'s `SchemaSnapshot`: where that
8
+ * captures the live *database* shape for the schema-parity gate, this
9
+ * captures the live *RPC registry* shape for the action-manifest parity gate.
10
+ * Both are dumped over a daemon-token `_testing_*` introspection action
11
+ * (`_testing_action_manifest` here, `_testing_schema_snapshot` there) and
12
+ * diffed across the TS spine and the Rust `testing_spine_stub` so a
13
+ * method-set or per-method auth-shape divergence fails loud. This complements
14
+ * the in-repo `spine_method_coverage` gate: that proves *mounted ⟹ covered*;
15
+ * this proves *TS-mount-set ≡ Rust-mount-set* (method set + auth shape).
16
+ *
17
+ * **Scope — domain + testing surface, not wire protocol.** The manifest is
18
+ * built from exactly the spec list its caller passes
19
+ * (`build_full_spine_rpc_actions` on the TS spine; the spec list minus
20
+ * `PROTOCOL_ACTION_SPECS` on the Rust stub). The protocol actions
21
+ * (`heartbeat` / `cancel`) are excluded by the caller on purpose: the two
22
+ * impls organize them differently (the TS spine mounts them on the WS
23
+ * endpoint only, never the HTTP-RPC endpoint; the Rust stub compiles one
24
+ * shared registry serving both transports), so including them would be a
25
+ * spurious cross-impl diff. This matches the scope of the in-repo
26
+ * `spine_method_coverage` gate (also over `build_full_spine_rpc_actions`).
27
+ *
28
+ * Paired with `action_manifest_parity.ts` for the diff + assertion helpers.
29
+ *
30
+ * `$lib`-free by contract — reached by the spawned TS binary (via
31
+ * `testing_reset_actions.ts` → `full_spine_mount.ts`), so every import is
32
+ * relative.
33
+ *
34
+ * @module
35
+ */
36
+ import { z } from 'zod';
37
+ import { type RouteAuth } from '../../http/auth_shape.js';
38
+ import type { RequestResponseActionSpec } from '../../actions/action_spec.js';
39
+ /**
40
+ * One method's normalized RPC metadata — the cross-impl-comparable unit.
41
+ * `roles` / `credential_types` are always present + sorted (an absent gate
42
+ * and an empty list both serialize to `[]`) so the diff never trips on a
43
+ * `undefined`-vs-`[]` or declaration-order difference between impls; the
44
+ * auth axes reuse the canonical `AuthAxisState` enum.
45
+ */
46
+ export declare const ActionManifestEntry: z.ZodObject<{
47
+ /** RPC method name (`spec.method`). */
48
+ method: z.ZodString;
49
+ /** `true` when the action declares `side_effects` (mutation; POST-only on the RPC transport). */
50
+ side_effects: z.ZodBoolean;
51
+ /** The `account` auth axis. */
52
+ account: z.ZodEnum<{
53
+ optional: "optional";
54
+ none: "none";
55
+ required: "required";
56
+ }>;
57
+ /** The `actor` auth axis. */
58
+ actor: z.ZodEnum<{
59
+ optional: "optional";
60
+ none: "none";
61
+ required: "required";
62
+ }>;
63
+ /** Required roles (any-of), sorted; `[]` when the action declares no role gate. */
64
+ roles: z.ZodArray<z.ZodString>;
65
+ /** Permitted credential channels, sorted; `[]` when ungated (any credential). */
66
+ credential_types: z.ZodArray<z.ZodString>;
67
+ }, z.core.$strict>;
68
+ export type ActionManifestEntry = z.infer<typeof ActionManifestEntry>;
69
+ /** The full action manifest — every entry, sorted by `method`. */
70
+ export declare const ActionManifest: z.ZodObject<{
71
+ methods: z.ZodArray<z.ZodObject<{
72
+ /** RPC method name (`spec.method`). */
73
+ method: z.ZodString;
74
+ /** `true` when the action declares `side_effects` (mutation; POST-only on the RPC transport). */
75
+ side_effects: z.ZodBoolean;
76
+ /** The `account` auth axis. */
77
+ account: z.ZodEnum<{
78
+ optional: "optional";
79
+ none: "none";
80
+ required: "required";
81
+ }>;
82
+ /** The `actor` auth axis. */
83
+ actor: z.ZodEnum<{
84
+ optional: "optional";
85
+ none: "none";
86
+ required: "required";
87
+ }>;
88
+ /** Required roles (any-of), sorted; `[]` when the action declares no role gate. */
89
+ roles: z.ZodArray<z.ZodString>;
90
+ /** Permitted credential channels, sorted; `[]` when ungated (any credential). */
91
+ credential_types: z.ZodArray<z.ZodString>;
92
+ }, z.core.$strict>>;
93
+ }, z.core.$strict>;
94
+ export type ActionManifest = z.infer<typeof ActionManifest>;
95
+ /**
96
+ * Normalize one spec's auth + side-effects into a manifest entry. Pulls the
97
+ * four auth axes off `RouteAuth` (the same shape the Rust `AuthSpec` mirrors)
98
+ * and flattens optional `roles` / `credential_types` to sorted arrays.
99
+ */
100
+ export declare const action_manifest_entry: (spec: {
101
+ readonly method: string;
102
+ readonly auth: RouteAuth;
103
+ readonly side_effects: boolean;
104
+ }) => ActionManifestEntry;
105
+ /**
106
+ * Build the normalized `ActionManifest` from a list of request-response
107
+ * specs. Entries are sorted by `method` so two impls producing the same set
108
+ * serialize identically regardless of mount order. The caller owns the scope
109
+ * it passes (the TS spine passes its full mount; the Rust stub filters
110
+ * `PROTOCOL_ACTION_SPECS` first) — see the module doc.
111
+ */
112
+ export declare const build_action_manifest: (specs: ReadonlyArray<Pick<RequestResponseActionSpec, "method" | "auth" | "side_effects">>) => ActionManifest;
113
+ //# sourceMappingURL=action_manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action_manifest.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/action_manifest.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB,OAAO,EAAgB,KAAK,SAAS,EAAC,MAAM,0BAA0B,CAAC;AACvE,OAAO,KAAK,EAAC,yBAAyB,EAAC,MAAM,8BAA8B,CAAC;AAE5E;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB;IAC/B,uCAAuC;;IAEvC,iGAAiG;;IAEjG,+BAA+B;;;;;;IAE/B,6BAA6B;;;;;;IAE7B,mFAAmF;;IAEnF,iFAAiF;;kBAEhF,CAAC;AACH,MAAM,MAAM,mBAAmB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEtE,kEAAkE;AAClE,eAAO,MAAM,cAAc;;QAhB1B,uCAAuC;;QAEvC,iGAAiG;;QAEjG,+BAA+B;;;;;;QAE/B,6BAA6B;;;;;;QAE7B,mFAAmF;;QAEnF,iFAAiF;;;kBAQhF,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC;AAM5D;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAAI,MAAM;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,YAAY,EAAE,OAAO,CAAC;CAC/B,KAAG,mBAOF,CAAC;AAMH;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,GACjC,OAAO,aAAa,CAAC,IAAI,CAAC,yBAAyB,EAAE,QAAQ,GAAG,MAAM,GAAG,cAAc,CAAC,CAAC,KACvF,cAED,CAAC"}
@@ -0,0 +1,89 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-impl RPC **action-manifest** introspection — a normalized,
4
+ * JSON-serializable dump of a backend's live RPC method set, one entry per
5
+ * method carrying its auth shape + side-effect flag.
6
+ *
7
+ * The sibling of `schema_introspect.ts`'s `SchemaSnapshot`: where that
8
+ * captures the live *database* shape for the schema-parity gate, this
9
+ * captures the live *RPC registry* shape for the action-manifest parity gate.
10
+ * Both are dumped over a daemon-token `_testing_*` introspection action
11
+ * (`_testing_action_manifest` here, `_testing_schema_snapshot` there) and
12
+ * diffed across the TS spine and the Rust `testing_spine_stub` so a
13
+ * method-set or per-method auth-shape divergence fails loud. This complements
14
+ * the in-repo `spine_method_coverage` gate: that proves *mounted ⟹ covered*;
15
+ * this proves *TS-mount-set ≡ Rust-mount-set* (method set + auth shape).
16
+ *
17
+ * **Scope — domain + testing surface, not wire protocol.** The manifest is
18
+ * built from exactly the spec list its caller passes
19
+ * (`build_full_spine_rpc_actions` on the TS spine; the spec list minus
20
+ * `PROTOCOL_ACTION_SPECS` on the Rust stub). The protocol actions
21
+ * (`heartbeat` / `cancel`) are excluded by the caller on purpose: the two
22
+ * impls organize them differently (the TS spine mounts them on the WS
23
+ * endpoint only, never the HTTP-RPC endpoint; the Rust stub compiles one
24
+ * shared registry serving both transports), so including them would be a
25
+ * spurious cross-impl diff. This matches the scope of the in-repo
26
+ * `spine_method_coverage` gate (also over `build_full_spine_rpc_actions`).
27
+ *
28
+ * Paired with `action_manifest_parity.ts` for the diff + assertion helpers.
29
+ *
30
+ * `$lib`-free by contract — reached by the spawned TS binary (via
31
+ * `testing_reset_actions.ts` → `full_spine_mount.ts`), so every import is
32
+ * relative.
33
+ *
34
+ * @module
35
+ */
36
+ import { z } from 'zod';
37
+ import { AuthAxisState } from '../../http/auth_shape.js';
38
+ /**
39
+ * One method's normalized RPC metadata — the cross-impl-comparable unit.
40
+ * `roles` / `credential_types` are always present + sorted (an absent gate
41
+ * and an empty list both serialize to `[]`) so the diff never trips on a
42
+ * `undefined`-vs-`[]` or declaration-order difference between impls; the
43
+ * auth axes reuse the canonical `AuthAxisState` enum.
44
+ */
45
+ export const ActionManifestEntry = z.strictObject({
46
+ /** RPC method name (`spec.method`). */
47
+ method: z.string(),
48
+ /** `true` when the action declares `side_effects` (mutation; POST-only on the RPC transport). */
49
+ side_effects: z.boolean(),
50
+ /** The `account` auth axis. */
51
+ account: AuthAxisState,
52
+ /** The `actor` auth axis. */
53
+ actor: AuthAxisState,
54
+ /** Required roles (any-of), sorted; `[]` when the action declares no role gate. */
55
+ roles: z.array(z.string()),
56
+ /** Permitted credential channels, sorted; `[]` when ungated (any credential). */
57
+ credential_types: z.array(z.string()),
58
+ });
59
+ /** The full action manifest — every entry, sorted by `method`. */
60
+ export const ActionManifest = z.strictObject({
61
+ methods: z.array(ActionManifestEntry),
62
+ });
63
+ /** Sorted copy of a readonly string list (stable cross-impl ordering). */
64
+ const sorted_strings = (xs) => [...(xs ?? [])].sort();
65
+ /**
66
+ * Normalize one spec's auth + side-effects into a manifest entry. Pulls the
67
+ * four auth axes off `RouteAuth` (the same shape the Rust `AuthSpec` mirrors)
68
+ * and flattens optional `roles` / `credential_types` to sorted arrays.
69
+ */
70
+ export const action_manifest_entry = (spec) => ({
71
+ method: spec.method,
72
+ side_effects: spec.side_effects,
73
+ account: spec.auth.account,
74
+ actor: spec.auth.actor,
75
+ roles: sorted_strings(spec.auth.roles),
76
+ credential_types: sorted_strings(spec.auth.credential_types),
77
+ });
78
+ /** Deterministic byte-lexicographic method order — matches the Rust `str::cmp` the stub sorts by. */
79
+ const compare_method = (a, b) => a.method < b.method ? -1 : a.method > b.method ? 1 : 0;
80
+ /**
81
+ * Build the normalized `ActionManifest` from a list of request-response
82
+ * specs. Entries are sorted by `method` so two impls producing the same set
83
+ * serialize identically regardless of mount order. The caller owns the scope
84
+ * it passes (the TS spine passes its full mount; the Rust stub filters
85
+ * `PROTOCOL_ACTION_SPECS` first) — see the module doc.
86
+ */
87
+ export const build_action_manifest = (specs) => ({
88
+ methods: specs.map(action_manifest_entry).sort(compare_method),
89
+ });
@@ -0,0 +1,67 @@
1
+ import '../assert_dev_env.js';
2
+ /**
3
+ * Cross-impl action-manifest parity — structural diff + assertion over two
4
+ * `ActionManifest`s captured via the `_testing_action_manifest` RPC action.
5
+ *
6
+ * The action-surface twin of `schema_parity.ts`: two live impls (the TS
7
+ * fuz_app spine and the Rust `testing_spine_stub`) are each other's parity
8
+ * reference. After both bootstrap, dump each one's live RPC method set, diff,
9
+ * fail loudly on drift. The diff entries name the specific divergence (a
10
+ * method only one impl mounts, a per-method auth-axis or side-effect
11
+ * mismatch) so the error message points at the source.
12
+ *
13
+ * ```ts
14
+ * const manifest_a = await capture_action_manifest(ts_handle);
15
+ * const manifest_b = await capture_action_manifest(rust_handle);
16
+ * assert_action_manifests_equal(manifest_a, manifest_b, {a: 'ts', b: 'rust'});
17
+ * ```
18
+ *
19
+ * Non-coverage: the manifest captures `{method, side_effects, account, actor,
20
+ * roles, credential_types}` — the wire-relevant auth shape. It does **not**
21
+ * capture input/output schemas (the declared-surface wire shape is gated by
22
+ * `rpc_round_trip`) nor the protocol actions `heartbeat` / `cancel` (excluded
23
+ * by construction — see `action_manifest.ts`).
24
+ *
25
+ * @module
26
+ */
27
+ import type { ActionManifest } from './action_manifest.js';
28
+ /** Structured drift entry. `where` is the named source impl ('a' or 'b'). */
29
+ export type ActionManifestDiff = {
30
+ readonly kind: 'method_only_in';
31
+ readonly where: 'a' | 'b';
32
+ readonly method: string;
33
+ } | {
34
+ readonly kind: 'side_effects_differ';
35
+ readonly method: string;
36
+ readonly a: boolean;
37
+ readonly b: boolean;
38
+ } | {
39
+ readonly kind: 'auth_field_differs';
40
+ readonly method: string;
41
+ readonly field: 'account' | 'actor' | 'roles' | 'credential_types';
42
+ readonly a: unknown;
43
+ readonly b: unknown;
44
+ };
45
+ /**
46
+ * Structural diff between two manifests — empty array means parity holds.
47
+ *
48
+ * Order is deterministic: methods in sorted order, with each method's
49
+ * side-effect + auth-field sub-diffs grouped together.
50
+ */
51
+ export declare const diff_action_manifests: (a: ActionManifest, b: ActionManifest) => Array<ActionManifestDiff>;
52
+ /** Labels used in formatted output — defaults to `'a'` and `'b'`. */
53
+ export interface ActionManifestDiffLabels {
54
+ readonly a?: string;
55
+ readonly b?: string;
56
+ }
57
+ /**
58
+ * Render a diff list as a human-readable multi-line string. Empty diffs
59
+ * produce an empty string.
60
+ */
61
+ export declare const format_action_manifest_diffs: (diffs: ReadonlyArray<ActionManifestDiff>, labels?: ActionManifestDiffLabels) => string;
62
+ /**
63
+ * Throw if the two manifests disagree. The error message names the impls
64
+ * (via `labels`) and lists every diff, so the failure is self-diagnosing.
65
+ */
66
+ export declare const assert_action_manifests_equal: (a: ActionManifest, b: ActionManifest, labels?: ActionManifestDiffLabels) => void;
67
+ //# sourceMappingURL=action_manifest_parity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"action_manifest_parity.d.ts","sourceRoot":"../src/lib/","sources":["../../../src/lib/testing/cross_backend/action_manifest_parity.ts"],"names":[],"mappings":"AAAA,OAAO,sBAAsB,CAAC;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAC,cAAc,EAAsB,MAAM,sBAAsB,CAAC;AAW9E,6EAA6E;AAC7E,MAAM,MAAM,kBAAkB,GAC3B;IAAC,QAAQ,CAAC,IAAI,EAAE,gBAAgB,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,GAAG,GAAG,GAAG,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;CAAC,GACrF;IACA,QAAQ,CAAC,IAAI,EAAE,qBAAqB,CAAC;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;CACnB,GACD;IACA,QAAQ,CAAC,IAAI,EAAE,oBAAoB,CAAC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,kBAAkB,CAAC;IACnE,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC;AAML;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,GACjC,GAAG,cAAc,EACjB,GAAG,cAAc,KACf,KAAK,CAAC,kBAAkB,CA+B1B,CAAC;AAEF,qEAAqE;AACrE,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;GAGG;AACH,eAAO,MAAM,4BAA4B,GACxC,OAAO,aAAa,CAAC,kBAAkB,CAAC,EACxC,SAAQ,wBAA6B,KACnC,MA4BF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,6BAA6B,GACzC,GAAG,cAAc,EACjB,GAAG,cAAc,EACjB,SAAQ,wBAA6B,KACnC,IAQF,CAAC"}
@@ -0,0 +1,88 @@
1
+ import '../assert_dev_env.js';
2
+ /** The auth axes compared as scalars (`'none' | 'optional' | 'required'`). */
3
+ const AUTH_SCALAR_FIELDS = ['account', 'actor'];
4
+ /** The auth axes compared as sorted string lists. */
5
+ const AUTH_LIST_FIELDS = ['roles', 'credential_types'];
6
+ /** Positional string-list equality (entries are pre-sorted by `build_action_manifest`). */
7
+ const string_lists_equal = (a, b) => a.length === b.length && a.every((v, i) => v === b[i]);
8
+ /**
9
+ * Structural diff between two manifests — empty array means parity holds.
10
+ *
11
+ * Order is deterministic: methods in sorted order, with each method's
12
+ * side-effect + auth-field sub-diffs grouped together.
13
+ */
14
+ export const diff_action_manifests = (a, b) => {
15
+ const diffs = [];
16
+ const a_by = new Map(a.methods.map((m) => [m.method, m]));
17
+ const b_by = new Map(b.methods.map((m) => [m.method, m]));
18
+ const all_methods = new Set([...a_by.keys(), ...b_by.keys()]);
19
+ for (const method of [...all_methods].sort()) {
20
+ const ea = a_by.get(method);
21
+ const eb = b_by.get(method);
22
+ if (!ea) {
23
+ diffs.push({ kind: 'method_only_in', where: 'b', method });
24
+ continue;
25
+ }
26
+ if (!eb) {
27
+ diffs.push({ kind: 'method_only_in', where: 'a', method });
28
+ continue;
29
+ }
30
+ if (ea.side_effects !== eb.side_effects) {
31
+ diffs.push({ kind: 'side_effects_differ', method, a: ea.side_effects, b: eb.side_effects });
32
+ }
33
+ for (const field of AUTH_SCALAR_FIELDS) {
34
+ if (ea[field] !== eb[field]) {
35
+ diffs.push({ kind: 'auth_field_differs', method, field, a: ea[field], b: eb[field] });
36
+ }
37
+ }
38
+ for (const field of AUTH_LIST_FIELDS) {
39
+ if (!string_lists_equal(ea[field], eb[field])) {
40
+ diffs.push({ kind: 'auth_field_differs', method, field, a: ea[field], b: eb[field] });
41
+ }
42
+ }
43
+ }
44
+ return diffs;
45
+ };
46
+ /**
47
+ * Render a diff list as a human-readable multi-line string. Empty diffs
48
+ * produce an empty string.
49
+ */
50
+ export const format_action_manifest_diffs = (diffs, labels = {}) => {
51
+ if (diffs.length === 0)
52
+ return '';
53
+ const label_a = labels.a ?? 'a';
54
+ const label_b = labels.b ?? 'b';
55
+ const where_label = (where) => (where === 'a' ? label_a : label_b);
56
+ const lines = [];
57
+ for (const d of diffs) {
58
+ switch (d.kind) {
59
+ case 'method_only_in':
60
+ lines.push(` method ${d.method} only in ${where_label(d.where)}`);
61
+ break;
62
+ case 'side_effects_differ':
63
+ lines.push(` ${d.method} side_effects differs: ${label_a}=${d.a}, ${label_b}=${d.b}`);
64
+ break;
65
+ case 'auth_field_differs':
66
+ lines.push(` ${d.method} auth.${d.field} differs: ${label_a}=${JSON.stringify(d.a)}, ${label_b}=${JSON.stringify(d.b)}`);
67
+ break;
68
+ default:
69
+ // Compile-time exhaustiveness — a new variant without a case here
70
+ // makes `d` non-never and fails type-check.
71
+ d;
72
+ break;
73
+ }
74
+ }
75
+ return lines.join('\n');
76
+ };
77
+ /**
78
+ * Throw if the two manifests disagree. The error message names the impls
79
+ * (via `labels`) and lists every diff, so the failure is self-diagnosing.
80
+ */
81
+ export const assert_action_manifests_equal = (a, b, labels = {}) => {
82
+ const diffs = diff_action_manifests(a, b);
83
+ if (diffs.length === 0)
84
+ return;
85
+ const label_a = labels.a ?? 'a';
86
+ const label_b = labels.b ?? 'b';
87
+ throw new Error(`Action-manifest parity failed: ${diffs.length} diff(s) between ${label_a} and ${label_b}\n${format_action_manifest_diffs(diffs, labels)}`);
88
+ };