@fuzdev/fuz_app 0.102.0 → 0.104.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 (49) hide show
  1. package/dist/auth/CLAUDE.md +37 -10
  2. package/dist/auth/cell_action_specs.d.ts +261 -35
  3. package/dist/auth/cell_action_specs.d.ts.map +1 -1
  4. package/dist/auth/cell_action_specs.js +102 -9
  5. package/dist/auth/cell_actions.d.ts +51 -0
  6. package/dist/auth/cell_actions.d.ts.map +1 -1
  7. package/dist/auth/cell_actions.js +168 -22
  8. package/dist/auth/cell_audit_events.d.ts +1 -1
  9. package/dist/auth/cell_audit_events.d.ts.map +1 -1
  10. package/dist/auth/cell_audit_events.js +3 -2
  11. package/dist/auth/cell_audit_metadata.d.ts +15 -0
  12. package/dist/auth/cell_audit_metadata.d.ts.map +1 -1
  13. package/dist/auth/cell_audit_metadata.js +11 -0
  14. package/dist/auth/cell_data_schema.d.ts +14 -16
  15. package/dist/auth/cell_data_schema.d.ts.map +1 -1
  16. package/dist/auth/cell_data_schema.js +14 -16
  17. package/dist/db/CLAUDE.md +19 -6
  18. package/dist/db/cell_ddl.d.ts +24 -2
  19. package/dist/db/cell_ddl.d.ts.map +1 -1
  20. package/dist/db/cell_ddl.js +33 -1
  21. package/dist/db/cell_queries.d.ts +59 -8
  22. package/dist/db/cell_queries.d.ts.map +1 -1
  23. package/dist/db/cell_queries.js +50 -12
  24. package/dist/testing/CLAUDE.md +37 -7
  25. package/dist/testing/cross_backend/capabilities.d.ts +14 -0
  26. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  27. package/dist/testing/cross_backend/capabilities.js +3 -0
  28. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
  29. package/dist/testing/cross_backend/cell_crud.js +55 -13
  30. package/dist/testing/cross_backend/cell_gated_create.d.ts +28 -0
  31. package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -0
  32. package/dist/testing/cross_backend/cell_gated_create.js +213 -0
  33. package/dist/testing/cross_backend/cell_grant_role.js +4 -4
  34. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
  35. package/dist/testing/cross_backend/cell_relations.js +30 -24
  36. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  37. package/dist/testing/cross_backend/default_backend_configs.js +7 -0
  38. package/dist/testing/cross_backend/expected_schema.json +4 -0
  39. package/dist/testing/cross_backend/fact_serving.js +2 -2
  40. package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
  41. package/dist/testing/cross_backend/full_spine_mount.js +9 -1
  42. package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
  43. package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -3
  44. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +22 -0
  45. package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -0
  46. package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +75 -0
  47. package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
  48. package/dist/testing/cross_backend/ts_spine_backend_config.js +5 -2
  49. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"cell_audit_metadata.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_audit_metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB;;;;;;iBAMjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
1
+ {"version":3,"file":"cell_audit_metadata.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_audit_metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB;;;;;;iBAMjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB;;;;;;;iBAIpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
@@ -44,3 +44,14 @@ export const CellCloneAuditMetadata = z.looseObject({
44
44
  item_count: z.number().int().nonnegative(),
45
45
  kind: z.string().optional(),
46
46
  });
47
+ /**
48
+ * Metadata envelope for `cell_moderate`. `cell_id` is the moderated
49
+ * contribution; `root_id` is its governing root (the container whose
50
+ * moderation authority gated the call); `moderation` is the terminal decision
51
+ * applied (`'approved'` | `'rejected'`).
52
+ */
53
+ export const CellModerateAuditMetadata = z.looseObject({
54
+ cell_id: Uuid,
55
+ root_id: Uuid,
56
+ moderation: z.enum(['approved', 'rejected']),
57
+ });
@@ -11,17 +11,19 @@
11
11
  * `auth/cell_actions.ts`.
12
12
  *
13
13
  * **Discipline**: a field joins `CellData` only when at least two
14
- * consumers in different domains read it generically. `kind` (editor
15
- * dispatch + sub-API registry), `label` (list/index rendering), and
16
- * `summary` (card subtitle + share-target description) meet this bar.
17
- * Future candidates require evidence of generic usage — otherwise they
18
- * stay per-kind.
14
+ * consumers in different domains read it generically. `label` (list/index
15
+ * rendering) and `summary` (card subtitle + share-target description) meet
16
+ * this bar. Future candidates require evidence of generic usage — otherwise
17
+ * they stay per-kind.
19
18
  *
20
- * **Visibility is not in here.** Access control is a peer of `cell_grant`,
21
- * not content metadata — `cell.visibility` lives as a top-level column on
22
- * `CellJson` and `CellRow` (the `CellVisibility` enum is defined in
23
- * `auth/cell_action_specs.ts` next to the wire fields that use it), and is
24
- * enforced by `can_view_cell` reading the column directly (no JSON dive).
19
+ * **`kind` is not in here, and is rejected if present.** Like `visibility`,
20
+ * a cell's `kind` is a top-level column (`cell.kind` on `CellJson` /
21
+ * `CellRow`), not content metadata — it is the capability / identity axis a
22
+ * creation authorizer gates on, write-once at birth. A stray `kind` key
23
+ * inside `data` is a fail-loud `ERROR_CELL_KIND_IN_DATA` at the create /
24
+ * update / clone-patch boundary (single source of truth). Access control
25
+ * (`cell.visibility`) is likewise a peer column, enforced by `can_view_cell`
26
+ * reading the column directly (no JSON dive).
25
27
  *
26
28
  * @module
27
29
  */
@@ -29,14 +31,10 @@ import { z } from 'zod';
29
31
  /**
30
32
  * Base cell-data shape. All fields optional; loose mode admits arbitrary
31
33
  * additional keys so apps can attach metadata or stage new kinds without
32
- * touching the wire schema.
33
- *
34
- * `kind` is optional because cells without a registered kind are valid —
35
- * admin-curated content, in-development types, or unknown shapes pass
36
- * through. Known kinds get richer validation via the per-kind sub-API.
34
+ * touching the wire schema. `kind` lives on the top-level `cell.kind`
35
+ * column, not here (see the module doc).
37
36
  */
38
37
  export declare const CellData: z.ZodObject<{
39
- kind: z.ZodOptional<z.ZodString>;
40
38
  label: z.ZodOptional<z.ZodString>;
41
39
  summary: z.ZodOptional<z.ZodString>;
42
40
  }, z.core.$loose>;
@@ -1 +1 @@
1
- {"version":3,"file":"cell_data_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_data_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;;;;;GAQG;AACH,eAAO,MAAM,QAAQ;;;;iBAInB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC"}
1
+ {"version":3,"file":"cell_data_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_data_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;;GAKG;AACH,eAAO,MAAM,QAAQ;;;iBAGnB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC"}
@@ -11,17 +11,19 @@
11
11
  * `auth/cell_actions.ts`.
12
12
  *
13
13
  * **Discipline**: a field joins `CellData` only when at least two
14
- * consumers in different domains read it generically. `kind` (editor
15
- * dispatch + sub-API registry), `label` (list/index rendering), and
16
- * `summary` (card subtitle + share-target description) meet this bar.
17
- * Future candidates require evidence of generic usage — otherwise they
18
- * stay per-kind.
14
+ * consumers in different domains read it generically. `label` (list/index
15
+ * rendering) and `summary` (card subtitle + share-target description) meet
16
+ * this bar. Future candidates require evidence of generic usage — otherwise
17
+ * they stay per-kind.
19
18
  *
20
- * **Visibility is not in here.** Access control is a peer of `cell_grant`,
21
- * not content metadata — `cell.visibility` lives as a top-level column on
22
- * `CellJson` and `CellRow` (the `CellVisibility` enum is defined in
23
- * `auth/cell_action_specs.ts` next to the wire fields that use it), and is
24
- * enforced by `can_view_cell` reading the column directly (no JSON dive).
19
+ * **`kind` is not in here, and is rejected if present.** Like `visibility`,
20
+ * a cell's `kind` is a top-level column (`cell.kind` on `CellJson` /
21
+ * `CellRow`), not content metadata — it is the capability / identity axis a
22
+ * creation authorizer gates on, write-once at birth. A stray `kind` key
23
+ * inside `data` is a fail-loud `ERROR_CELL_KIND_IN_DATA` at the create /
24
+ * update / clone-patch boundary (single source of truth). Access control
25
+ * (`cell.visibility`) is likewise a peer column, enforced by `can_view_cell`
26
+ * reading the column directly (no JSON dive).
25
27
  *
26
28
  * @module
27
29
  */
@@ -29,14 +31,10 @@ import { z } from 'zod';
29
31
  /**
30
32
  * Base cell-data shape. All fields optional; loose mode admits arbitrary
31
33
  * additional keys so apps can attach metadata or stage new kinds without
32
- * touching the wire schema.
33
- *
34
- * `kind` is optional because cells without a registered kind are valid —
35
- * admin-curated content, in-development types, or unknown shapes pass
36
- * through. Known kinds get richer validation via the per-kind sub-API.
34
+ * touching the wire schema. `kind` lives on the top-level `cell.kind`
35
+ * column, not here (see the module doc).
37
36
  */
38
37
  export const CellData = z.looseObject({
39
- kind: z.string().optional(),
40
38
  label: z.string().optional(),
41
39
  summary: z.string().optional(),
42
40
  });
package/dist/db/CLAUDE.md CHANGED
@@ -51,18 +51,31 @@ The wire schemas + RPC handlers + authz predicates for this layer live in
51
51
  tables; single-migration `CELL_MIGRATION_NS` (namespace `fuz_cell`).
52
52
  `cell.visibility cell_visibility NOT NULL DEFAULT 'private'` (PG ENUM
53
53
  `('private', 'public')`) — a top-level access-control column, peer to
54
- `cell_grant`, not a `data` field. Nullable `created_by` / `updated_by`
55
- FKs to `actor` (NULL = system origin). GIN on `data` / `refs`; global
56
- partial-unique on `path`.
54
+ `cell_grant`, not a `data` field. `cell.kind text` (nullable) is the
55
+ write-once capability/identity axis — likewise a top-level column, not a
56
+ `data` field (`idx_cell_kind` partial-active). `cell.parent_id` / `root_id`
57
+ (nullable self-FKs to `cell.id`) are the **directory tree** — immediate
58
+ container + governing root (`root_id = parent.root_id ?? parent.id`, null for a
59
+ root), immutable in v1 (`idx_cell_root` partial-active for subtree feeds).
60
+ `cell.moderation text` (nullable — `pending` / `approved` / `rejected`) is the
61
+ approval-lifecycle marker, peer to `visibility` (partial `idx_cell_moderation_pending`
62
+ on `root_id WHERE moderation = 'pending'` for the mod queue), set by the create
63
+ authorizer's verdict + the `cell_moderate` verb, never in `data`. Nullable
64
+ `created_by` / `updated_by` FKs to `actor` (NULL = system origin). GIN on `data`
65
+ / `refs`; global partial-unique on `path`.
57
66
  - **`cell_history_ddl.ts`** — dormant `cell_history` table
58
67
  (`CELL_HISTORY_MIGRATION_NS`, namespace `fuz_cell_history`), FK → `cell.id`.
59
68
  Ships present-but-unwritten; no snapshot lifecycle yet.
60
69
  - **`cell_queries.ts`** — `query_cell_create / get / get_by_path / update /
61
- delete`, `_list_by_data_kind / _list_by_creator`, the
70
+ delete / set_moderation`, `_list_by_kind / _list_by_creator`, the
62
71
  generic `query_cell_list` (filter + SQL-side visibility predicate mirroring
63
- `can_view_cell`; the `ref` filter narrows by `cell.refs`), and
72
+ `can_view_cell`; the `kind` filter narrows by the `cell.kind` column, the
73
+ `ref` filter by `cell.refs`, plus `root_id` / `moderation` filters for the
74
+ per-root feed + mod queue), and
64
75
  `query_cell_load_many` (bulk id load, no visibility filter — feeds the
65
- strict relation-read filter). `cell.refs` derived from
76
+ strict relation-read filter). `query_cell_set_moderation` is the dedicated
77
+ `cell_moderate` write (`moderation` is absent from `CellUpdatePatch`, so an
78
+ author can't self-approve via `cell_update`). `cell.refs` derived from
66
79
  `data` via `extract_refs` on create/update. `CellRow.grant_count` is a
67
80
  derived projection (correlated subquery on `idx_cell_grant_cell`).
68
81
  - **`cell_grant_queries.ts`** — resource-side ACL: `query_cell_grant_create`
@@ -62,8 +62,24 @@ export declare const CELL_VISIBILITY_TYPE = "\nDO $$ BEGIN\n\tCREATE TYPE cell_v
62
62
  *
63
63
  * `path` is the global namespace axis (no tenant/hub scoping) — globally
64
64
  * unique on active rows via `idx_cell_path_unique`.
65
+ *
66
+ * `kind` is the capability / identity axis — a nullable top-level column
67
+ * (peer to `visibility` / `path`), **not** a field inside `data`. It is the
68
+ * discriminator a creation authorizer gates on (see `auth/cell_actions.ts`
69
+ * `CellCreateAuthorize`) and is **write-once**: set at INSERT and carried on
70
+ * no update path, so a cell's kind is fixed at birth. Content stays
71
+ * duck-typed in `data`; `kind` is a capability tag, not a content-type.
72
+ *
73
+ * `parent_id` / `root_id` are the **directory tree** (containment): `parent_id`
74
+ * is the immediate container (nullable self-FK; `NULL` = a root), `root_id` is
75
+ * the governing root denormalized for flat-subtree queries (`root_id =
76
+ * parent.root_id ?? parent.id`, so a root has `NULL`). Both are set once at
77
+ * create and immutable in v1 (carried on no update path). `moderation`
78
+ * (nullable text — `pending` / `approved` / `rejected`; `NULL` = unmoderated)
79
+ * is the approval-lifecycle marker, peer to `visibility` (a control field with
80
+ * a non-author writer — see `auth/cell_actions.ts`), never inside `data`.
65
81
  */
66
- export declare const CELL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS cell (\n\tid UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n\tdata JSONB NOT NULL,\n\tvisibility cell_visibility NOT NULL DEFAULT 'private',\n\tpath TEXT,\n\trefs TEXT[],\n\tcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n\tupdated_at TIMESTAMPTZ,\n\tdeleted_at TIMESTAMPTZ,\n\tcreated_by UUID REFERENCES actor(id) ON DELETE SET NULL,\n\tupdated_by UUID REFERENCES actor(id) ON DELETE SET NULL\n)";
82
+ export declare const CELL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS cell (\n\tid UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n\tdata JSONB NOT NULL,\n\tkind TEXT,\n\tvisibility cell_visibility NOT NULL DEFAULT 'private',\n\tpath TEXT,\n\trefs TEXT[],\n\tparent_id UUID REFERENCES cell(id) ON DELETE SET NULL,\n\troot_id UUID REFERENCES cell(id) ON DELETE SET NULL,\n\tmoderation TEXT,\n\tcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n\tupdated_at TIMESTAMPTZ,\n\tdeleted_at TIMESTAMPTZ,\n\tcreated_by UUID REFERENCES actor(id) ON DELETE SET NULL,\n\tupdated_by UUID REFERENCES actor(id) ON DELETE SET NULL\n)";
67
83
  /**
68
84
  * Cell indexes — all active-only, partial on `deleted_at IS NULL`.
69
85
  *
@@ -71,9 +87,15 @@ export declare const CELL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS cell (\n\tid UU
71
87
  * - `idx_cell_path_unique`: global `path` uniqueness + read-side path
72
88
  * lookup. Partial on path + active so reused paths after soft delete are
73
89
  * allowed.
74
- * - `idx_cell_data`: shape-driven queries (`data ? 'kind'`, `data @> ...`).
90
+ * - `idx_cell_kind`: the `cell_list` kind filter (`cell.kind = ?`) and
91
+ * kind-scoped scans. Active-only.
92
+ * - `idx_cell_data`: shape-driven queries (`data @> ...`).
75
93
  * - `idx_cell_refs`: cells-by-fact discovery (cross-cell reference graph).
76
94
  * - `idx_cell_created_by`: "cells this actor created" queries.
95
+ * - `idx_cell_root`: flat-subtree feed/scope queries by governing root
96
+ * (`cell.root_id = ?`). Active-only.
97
+ * - `idx_cell_moderation_pending`: the moderation queue — pending
98
+ * contributions per governing root. Partial on `moderation = 'pending'`.
77
99
  *
78
100
  * Parent↔child membership and named relations live in sibling tables;
79
101
  * see `CELL_ITEM_INDEXES` / `CELL_FIELD_INDEXES` below.
@@ -1 +1 @@
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"}
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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,WAAW,gkBAgBtB,CAAC;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,MAAM,CAkBtC,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"}
@@ -65,14 +65,34 @@ END $$`;
65
65
  *
66
66
  * `path` is the global namespace axis (no tenant/hub scoping) — globally
67
67
  * unique on active rows via `idx_cell_path_unique`.
68
+ *
69
+ * `kind` is the capability / identity axis — a nullable top-level column
70
+ * (peer to `visibility` / `path`), **not** a field inside `data`. It is the
71
+ * discriminator a creation authorizer gates on (see `auth/cell_actions.ts`
72
+ * `CellCreateAuthorize`) and is **write-once**: set at INSERT and carried on
73
+ * no update path, so a cell's kind is fixed at birth. Content stays
74
+ * duck-typed in `data`; `kind` is a capability tag, not a content-type.
75
+ *
76
+ * `parent_id` / `root_id` are the **directory tree** (containment): `parent_id`
77
+ * is the immediate container (nullable self-FK; `NULL` = a root), `root_id` is
78
+ * the governing root denormalized for flat-subtree queries (`root_id =
79
+ * parent.root_id ?? parent.id`, so a root has `NULL`). Both are set once at
80
+ * create and immutable in v1 (carried on no update path). `moderation`
81
+ * (nullable text — `pending` / `approved` / `rejected`; `NULL` = unmoderated)
82
+ * is the approval-lifecycle marker, peer to `visibility` (a control field with
83
+ * a non-author writer — see `auth/cell_actions.ts`), never inside `data`.
68
84
  */
69
85
  export const CELL_SCHEMA = `
70
86
  CREATE TABLE IF NOT EXISTS cell (
71
87
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
72
88
  data JSONB NOT NULL,
89
+ kind TEXT,
73
90
  visibility cell_visibility NOT NULL DEFAULT 'private',
74
91
  path TEXT,
75
92
  refs TEXT[],
93
+ parent_id UUID REFERENCES cell(id) ON DELETE SET NULL,
94
+ root_id UUID REFERENCES cell(id) ON DELETE SET NULL,
95
+ moderation TEXT,
76
96
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
77
97
  updated_at TIMESTAMPTZ,
78
98
  deleted_at TIMESTAMPTZ,
@@ -86,9 +106,15 @@ CREATE TABLE IF NOT EXISTS cell (
86
106
  * - `idx_cell_path_unique`: global `path` uniqueness + read-side path
87
107
  * lookup. Partial on path + active so reused paths after soft delete are
88
108
  * allowed.
89
- * - `idx_cell_data`: shape-driven queries (`data ? 'kind'`, `data @> ...`).
109
+ * - `idx_cell_kind`: the `cell_list` kind filter (`cell.kind = ?`) and
110
+ * kind-scoped scans. Active-only.
111
+ * - `idx_cell_data`: shape-driven queries (`data @> ...`).
90
112
  * - `idx_cell_refs`: cells-by-fact discovery (cross-cell reference graph).
91
113
  * - `idx_cell_created_by`: "cells this actor created" queries.
114
+ * - `idx_cell_root`: flat-subtree feed/scope queries by governing root
115
+ * (`cell.root_id = ?`). Active-only.
116
+ * - `idx_cell_moderation_pending`: the moderation queue — pending
117
+ * contributions per governing root. Partial on `moderation = 'pending'`.
92
118
  *
93
119
  * Parent↔child membership and named relations live in sibling tables;
94
120
  * see `CELL_ITEM_INDEXES` / `CELL_FIELD_INDEXES` below.
@@ -99,12 +125,18 @@ export const CELL_INDEXES = [
99
125
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_cell_path_unique
100
126
  ON cell(path)
101
127
  WHERE path IS NOT NULL AND deleted_at IS NULL`,
128
+ `CREATE INDEX IF NOT EXISTS idx_cell_kind ON cell(kind)
129
+ WHERE deleted_at IS NULL`,
102
130
  `CREATE INDEX IF NOT EXISTS idx_cell_data ON cell USING gin(data)
103
131
  WHERE deleted_at IS NULL`,
104
132
  `CREATE INDEX IF NOT EXISTS idx_cell_refs ON cell USING gin(refs)
105
133
  WHERE refs IS NOT NULL AND deleted_at IS NULL`,
106
134
  `CREATE INDEX IF NOT EXISTS idx_cell_created_by ON cell(created_by)
107
135
  WHERE deleted_at IS NULL`,
136
+ `CREATE INDEX IF NOT EXISTS idx_cell_root ON cell(root_id)
137
+ WHERE deleted_at IS NULL`,
138
+ `CREATE INDEX IF NOT EXISTS idx_cell_moderation_pending ON cell(root_id)
139
+ WHERE moderation = 'pending'`,
108
140
  ];
109
141
  /**
110
142
  * `cell_grant` table — resource-side ACL for cells. Each row admits a
@@ -42,9 +42,13 @@ import type { CellVisibility } from '../auth/cell_action_specs.js';
42
42
  export interface CellRow {
43
43
  id: Uuid;
44
44
  data: CellData;
45
+ kind: string | null;
45
46
  visibility: CellVisibility;
46
47
  path: string | null;
47
48
  refs: Array<FactHash> | null;
49
+ parent_id: Uuid | null;
50
+ root_id: Uuid | null;
51
+ moderation: string | null;
48
52
  created_at: Date;
49
53
  updated_at: Date | null;
50
54
  deleted_at: Date | null;
@@ -52,11 +56,23 @@ export interface CellRow {
52
56
  updated_by: Uuid | null;
53
57
  grant_count: number;
54
58
  }
55
- /** Input for `query_cell_create`. `refs` is derived from `data`. */
59
+ /**
60
+ * Input for `query_cell_create`. `refs` is derived from `data`. `kind` is
61
+ * the write-once capability axis (`cell.kind` column) — set here at INSERT
62
+ * and absent from `CellUpdatePatch`, so it can never change post-create.
63
+ * `parent_id` / `root_id` / `moderation` are the directory-tree + lifecycle
64
+ * columns the handler derives (parent containment, governing root, the
65
+ * create-authorizer verdict) — likewise write-once / control-gated, never on
66
+ * `CellUpdatePatch`.
67
+ */
56
68
  export interface CellCreateQueryInput {
57
69
  data: Json;
70
+ kind?: string | null;
58
71
  visibility?: CellVisibility;
59
72
  path?: string | null;
73
+ parent_id?: Uuid | null;
74
+ root_id?: Uuid | null;
75
+ moderation?: string | null;
60
76
  created_by?: Uuid | null;
61
77
  }
62
78
  /**
@@ -153,15 +169,37 @@ export declare const query_cell_delete: (deps: QueryDeps, id: Uuid, options?: {
153
169
  deleted_by?: Uuid | null;
154
170
  }) => Promise<boolean>;
155
171
  /**
156
- * List cells whose `data.kind` matches the given value, newest first.
157
- * Uses the `idx_cell_data` GIN index (`data @> ...`).
172
+ * Transition a contribution's `moderation` marker (the `cell_moderate` verb's
173
+ * write). On `set_visibility_public` (an approval) it also flips `visibility`
174
+ * to `'public'` so the approved contribution goes live; a rejection leaves
175
+ * visibility untouched (stays private). `updated_at` / `updated_by` are stamped.
176
+ *
177
+ * `moderation` is deliberately **not** writable through `query_cell_update`
178
+ * (it's absent from `CellUpdatePatch`) — gating the transition on a dedicated
179
+ * query (with the manage-tier authority check in the handler) is what stops an
180
+ * author self-approving their own pending cell.
181
+ *
182
+ * @param deps - query deps
183
+ * @param id - cell id
184
+ * @param moderation - the terminal marker to write (`'approved'` | `'rejected'`)
185
+ * @param options - `set_visibility_public` flips visibility on approval; `updated_by` records the moderator
186
+ * @returns the updated row, or `null` when no active row matched (raced delete)
187
+ * @mutates `cell` - sets `moderation` (+ maybe `visibility`) on one row
188
+ */
189
+ export declare const query_cell_set_moderation: (deps: QueryDeps, id: Uuid, moderation: string, options: {
190
+ set_visibility_public: boolean;
191
+ updated_by?: Uuid | null;
192
+ }) => Promise<CellRow | null>;
193
+ /**
194
+ * List active cells with the given `kind`, newest first. Uses the
195
+ * `idx_cell_kind` index (`cell.kind = ?`).
158
196
  *
159
197
  * @param deps - query deps
160
- * @param kind - `data.kind` value to match (e.g. `'collection'`, `'entry'`)
198
+ * @param kind - `cell.kind` value to match (e.g. `'collection'`, `'entry'`)
161
199
  * @param options - pagination
162
200
  * @returns matching active rows
163
201
  */
164
- export declare const query_cell_list_by_data_kind: (deps: QueryDeps, kind: string, options?: Pick<CellListOptions, "limit" | "offset">) => Promise<Array<CellRow>>;
202
+ export declare const query_cell_list_by_kind: (deps: QueryDeps, kind: string, options?: Pick<CellListOptions, "limit" | "offset">) => Promise<Array<CellRow>>;
165
203
  /**
166
204
  * List active cells created by an actor, newest first. Backed by the
167
205
  * `idx_cell_created_by` partial index.
@@ -221,8 +259,8 @@ export declare const query_cell_list_by_creator: (deps: QueryDeps, actor_id: Uui
221
259
  export declare const query_cell_list: (deps: QueryDeps, params: CellListParams) => Promise<Array<CellRow>>;
222
260
  /** Parameters for `query_cell_list`. All filter dimensions are optional. */
223
261
  export interface CellListParams {
224
- /** Match `data.kind = ?` via `data @> {"kind": ?}` (uses `idx_cell_data`). */
225
- data_kind?: string;
262
+ /** Match `cell.kind = ?` (uses `idx_cell_kind`). */
263
+ kind?: string;
226
264
  /**
227
265
  * Match `cell.visibility = ?` directly on the top-level column.
228
266
  * Additional narrowing on top of the SQL-side auth visibility
@@ -240,6 +278,19 @@ export interface CellListParams {
240
278
  * literal matching.
241
279
  */
242
280
  path_prefix?: string;
281
+ /** Scope to a directory subtree by governing root (`cell.root_id = ?`, uses `idx_cell_root`). */
282
+ root_id?: Uuid;
283
+ /**
284
+ * Filter by moderation lifecycle marker (`'pending'` drives the mod queue via
285
+ * `idx_cell_moderation_pending`).
286
+ *
287
+ * Note: the visibility predicate still applies, so a `'pending'` (private)
288
+ * queue surfaces only to viewers it admits — admin / owner / grant. A
289
+ * non-admin container manager won't see pending children here until a
290
+ * manager-scoped visibility branch is added, so for now the queue is an admin
291
+ * surface (`cell_moderate` of a known id still works regardless).
292
+ */
293
+ moderation?: string;
243
294
  /**
244
295
  * Batch-fetch by id. The visibility predicate still runs, so callers
245
296
  * passing ids they can't view simply get fewer rows back. Order of
@@ -286,7 +337,7 @@ export interface CellListParams {
286
337
  /**
287
338
  * When `true`, narrow to cells admitting the caller via a
288
339
  * `cell_grant` row AND that the caller does not own. Authenticated
289
- * only (`viewer_actor_id` must be set). Combine with `data_kind` /
340
+ * only (`viewer_actor_id` must be set). Combine with `kind` /
290
341
  * `path_prefix` etc. to scope further.
291
342
  */
292
343
  shared_with_caller_only?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"cell_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_queries.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cell_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_queries.ts"],"names":[],"mappings":"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"}
@@ -51,13 +51,17 @@ const grant_count_projection = (cell_alias) => `(SELECT COUNT(*)::int FROM cell_
51
51
  export const query_cell_create = async (deps, input) => {
52
52
  const refs = derive_refs(input.data);
53
53
  const row = await deps.db.query_one(`INSERT INTO cell
54
- (data, visibility, path, refs, created_by)
55
- VALUES ($1::jsonb, COALESCE($2::cell_visibility, 'private'::cell_visibility), $3, $4::text[], $5)
54
+ (data, kind, visibility, path, refs, parent_id, root_id, moderation, created_by)
55
+ VALUES ($1::jsonb, $2, COALESCE($3::cell_visibility, 'private'::cell_visibility), $4, $5::text[], $6, $7, $8, $9)
56
56
  RETURNING *, ${grant_count_projection('cell')}`, [
57
57
  JSON.stringify(input.data),
58
+ input.kind ?? null,
58
59
  input.visibility ?? null,
59
60
  input.path ?? null,
60
61
  refs,
62
+ input.parent_id ?? null,
63
+ input.root_id ?? null,
64
+ input.moderation ?? null,
61
65
  input.created_by ?? null,
62
66
  ]);
63
67
  return assert_row(row, 'INSERT INTO cell');
@@ -171,20 +175,48 @@ export const query_cell_delete = async (deps, id, options) => {
171
175
  return row !== undefined;
172
176
  };
173
177
  /**
174
- * List cells whose `data.kind` matches the given value, newest first.
175
- * Uses the `idx_cell_data` GIN index (`data @> ...`).
178
+ * Transition a contribution's `moderation` marker (the `cell_moderate` verb's
179
+ * write). On `set_visibility_public` (an approval) it also flips `visibility`
180
+ * to `'public'` so the approved contribution goes live; a rejection leaves
181
+ * visibility untouched (stays private). `updated_at` / `updated_by` are stamped.
182
+ *
183
+ * `moderation` is deliberately **not** writable through `query_cell_update`
184
+ * (it's absent from `CellUpdatePatch`) — gating the transition on a dedicated
185
+ * query (with the manage-tier authority check in the handler) is what stops an
186
+ * author self-approving their own pending cell.
187
+ *
188
+ * @param deps - query deps
189
+ * @param id - cell id
190
+ * @param moderation - the terminal marker to write (`'approved'` | `'rejected'`)
191
+ * @param options - `set_visibility_public` flips visibility on approval; `updated_by` records the moderator
192
+ * @returns the updated row, or `null` when no active row matched (raced delete)
193
+ * @mutates `cell` - sets `moderation` (+ maybe `visibility`) on one row
194
+ */
195
+ export const query_cell_set_moderation = async (deps, id, moderation, options) => {
196
+ const row = await deps.db.query_one(`UPDATE cell SET
197
+ moderation = $2,
198
+ visibility = CASE WHEN $3::bool THEN 'public'::cell_visibility ELSE visibility END,
199
+ updated_by = $4,
200
+ updated_at = NOW()
201
+ WHERE id = $1 AND deleted_at IS NULL
202
+ RETURNING *, ${grant_count_projection('cell')}`, [id, moderation, options.set_visibility_public, options.updated_by ?? null]);
203
+ return row ?? null;
204
+ };
205
+ /**
206
+ * List active cells with the given `kind`, newest first. Uses the
207
+ * `idx_cell_kind` index (`cell.kind = ?`).
176
208
  *
177
209
  * @param deps - query deps
178
- * @param kind - `data.kind` value to match (e.g. `'collection'`, `'entry'`)
210
+ * @param kind - `cell.kind` value to match (e.g. `'collection'`, `'entry'`)
179
211
  * @param options - pagination
180
212
  * @returns matching active rows
181
213
  */
182
- export const query_cell_list_by_data_kind = async (deps, kind, options) => deps.db.query(`SELECT *, ${grant_count_projection('cell')}
214
+ export const query_cell_list_by_kind = async (deps, kind, options) => deps.db.query(`SELECT *, ${grant_count_projection('cell')}
183
215
  FROM cell
184
- WHERE data @> $1::jsonb
216
+ WHERE kind = $1
185
217
  AND deleted_at IS NULL
186
218
  ORDER BY created_at DESC
187
- LIMIT $2 OFFSET $3`, [JSON.stringify({ kind }), options?.limit ?? null, options?.offset ?? 0]);
219
+ LIMIT $2 OFFSET $3`, [kind, options?.limit ?? null, options?.offset ?? 0]);
188
220
  /**
189
221
  * List active cells created by an actor, newest first. Backed by the
190
222
  * `idx_cell_created_by` partial index.
@@ -271,7 +303,7 @@ export const query_cell_list = async (deps, params) => {
271
303
  // supplied wildcards (`%`, `_`, `\`) match literally — Postgres 11+ has
272
304
  // the function and pglite/PG 16 supports it.
273
305
  //
274
- // Two SQL shapes share the same 14-param positional layout:
306
+ // Two SQL shapes share the same 16-param positional layout:
275
307
  //
276
308
  // - `shared_with_caller_only: false` — cell-driven scan with the
277
309
  // visibility predicate (admin / public / owner / grant-admits).
@@ -284,7 +316,7 @@ export const query_cell_list = async (deps, params) => {
284
316
  : build_general_sql(order_column, order_direction);
285
317
  return deps.db.query(sql, [
286
318
  params.include_deleted === true,
287
- params.data_kind ?? null,
319
+ params.kind ?? null,
288
320
  params.ref ?? null,
289
321
  params.created_by ?? null,
290
322
  params.path_prefix ?? null,
@@ -297,6 +329,8 @@ export const query_cell_list = async (deps, params) => {
297
329
  role_grant_roles,
298
330
  role_grant_scope_ids,
299
331
  params.visibility ?? null,
332
+ params.root_id ?? null,
333
+ params.moderation ?? null,
300
334
  ]);
301
335
  };
302
336
  /**
@@ -333,12 +367,14 @@ const CALLER_ROLE_GRANTS_CTE = `WITH caller_role_grants AS (
333
367
  const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
334
368
  SELECT c.*, ${grant_count_projection('c')} FROM cell c
335
369
  WHERE ($1::bool OR c.deleted_at IS NULL)
336
- AND ($2::text IS NULL OR c.data @> jsonb_build_object('kind', $2::text))
370
+ AND ($2::text IS NULL OR c.kind = $2::text)
337
371
  AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
338
372
  AND ($3::text IS NULL OR c.refs @> ARRAY[$3]::text[])
339
373
  AND ($4::uuid IS NULL OR c.created_by = $4)
340
374
  AND ($5::text IS NULL OR starts_with(c.path, $5))
341
375
  AND ($10::uuid[] IS NULL OR c.id = ANY($10))
376
+ AND ($15::uuid IS NULL OR c.root_id = $15)
377
+ AND ($16::text IS NULL OR c.moderation = $16::text)
342
378
  AND (
343
379
  $6::bool
344
380
  OR c.visibility = 'public'
@@ -369,12 +405,14 @@ const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRAN
369
405
  const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
370
406
  SELECT c.*, ${grant_count_projection('c')} FROM cell c
371
407
  WHERE ($1::bool OR c.deleted_at IS NULL)
372
- AND ($2::text IS NULL OR c.data @> jsonb_build_object('kind', $2::text))
408
+ AND ($2::text IS NULL OR c.kind = $2::text)
373
409
  AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
374
410
  AND ($3::text IS NULL OR c.refs @> ARRAY[$3]::text[])
375
411
  AND ($4::uuid IS NULL OR c.created_by = $4)
376
412
  AND ($5::text IS NULL OR starts_with(c.path, $5))
377
413
  AND ($10::uuid[] IS NULL OR c.id = ANY($10))
414
+ AND ($15::uuid IS NULL OR c.root_id = $15)
415
+ AND ($16::text IS NULL OR c.moderation = $16::text)
378
416
  AND $7::uuid IS NOT NULL
379
417
  AND c.created_by IS DISTINCT FROM $7
380
418
  AND c.id IN (
@@ -897,8 +897,9 @@ source of truth for wire-shape conformance.
897
897
 
898
898
  - `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
899
899
  (the **gating** flags `ws` / `sse` / `cell_crud` / `cell_relations` /
900
- `account_lifecycle` / `fact_serving` / `ready` / `account_status` /
901
- `oversized_reject_closes_connection` / `peer_request` — each has a `test_if`
900
+ `cell_gated_create` / `account_lifecycle` / `fact_serving` / `ready` /
901
+ `account_status` / `oversized_reject_closes_connection` / `peer_request` —
902
+ each has a `test_if`
902
903
  reader; `peer_request` (server-initiated requests — the ActionPeer
903
904
  `peer/ping` round-trip) is `true` for both the Rust spine and the TS spine
904
905
  (`BackendWebsocketTransport.request_connection`); `ts_default_capabilities`
@@ -914,6 +915,15 @@ source of truth for wire-shape conformance.
914
915
  parity suite — both `true` on every backend that live-mounts the full cell
915
916
  surface (TS spine binary, in-process app, Rust stub). A backend mounting only
916
917
  plain CRUD would declare `cell_crud: true, cell_relations: false`.
918
+ `cell_gated_create` gates `describe_cell_gated_create_cross_tests` +
919
+ `describe_cell_moderate_cross_tests` (the parent-aware-creation-authorizer +
920
+ `cell_moderate` parity proofs); unlike `cell_crud`/`cell_relations`
921
+ it is **cross-only** (`false` in-process) — `true` only on the reference
922
+ spine binaries that live-mount the `test_cell_gated_create_authorize` policy
923
+ (the TS spine `full_spine_mount` + the Rust `testing_spine_stub`), since the
924
+ authorizer _behavior_ adds no method/wire shape and so the schema + manifest
925
+ parity gates are blind to an authorizer divergence (the `cell_moderate`
926
+ _verb_ it pairs with is on the manifest, separately gated).
917
927
  `account_lifecycle` gates `describe_account_lifecycle_cross_tests` (the
918
928
  `account_delete` / `account_undelete` / `account_purge` parity suite); unlike
919
929
  cells those verbs **are** on the declared surface (in `create_admin_actions`,
@@ -1154,7 +1164,7 @@ the stream \_can't* open on a spine without SSE — a self-cleaning tripwire for
1154
1164
  the spine that should grow it, distinct from the consumer-legit capability
1155
1165
  skip the shared suite emits.
1156
1166
 
1157
- ### `cross_backend/cell_crud.ts` + `cell_relations.ts` — cell parity suites
1167
+ ### `cross_backend/cell_crud.ts` + `cell_relations.ts` + `cell_gated_create.ts` — cell parity suites
1158
1168
 
1159
1169
  The cell-layer parity coverage is split across two sibling suites. Cells
1160
1170
  can't ride the generic `describe_rpc_round_trip_tests` (stateful verbs need a
@@ -1189,10 +1199,30 @@ backdoor, cell).
1189
1199
  holder who can `cell_get` still gets the IDOR 404). Only **actor-shaped**
1190
1200
  grants are exercised — role-shaped principals need a closed role registry the
1191
1201
  Rust spine deliberately lacks.
1192
-
1193
- Both gate `true` on TS + Rust (cells run on both, no `.skip`). Cross-process
1194
- wiring is `src/test/cross_backend/cell.cross.test.ts` (both suites); the
1195
- in-process legs (plain `gro test`) are `src/test/auth/cell_crud_parity.db.test.ts`
1202
+ - **`describe_cell_gated_create_cross_tests`** + **`describe_cell_moderate_cross_tests`**
1203
+ (gate on `capabilities.cell_gated_create`, **cross-only**) — the parent-aware
1204
+ cell-creation authorizer (`CellCreateAuthorize`, **directory model**) +
1205
+ the `cell_moderate` verb parity proofs. Both spines mount the same
1206
+ `test_cell_gated_create_authorize` policy: a `kind: 'space'` **root** is
1207
+ admin-only (a non-admin → **403** `cell_create_forbidden`); a **contribution**
1208
+ under a space is gated by the root's `data.policy[kind] = {min_role,
1209
+ moderation_required}`, with `moderation` set per the verdict (`pending`+private
1210
+ vs `approved`+author-visibility); a **hidden** parent **404**-masks vs a
1211
+ **visible** one **403**s; plain parentless creates stay open. The moderate
1212
+ suite then proves the `pending → approved | rejected` transition: an admin
1213
+ (root manager) approves → public, the **author** self-approve → 403
1214
+ (anti-self-approval), a non-viewer → 404, reject → stays private. The
1215
+ authorizer _behavior_ adds no method/wire shape, so schema + manifest parity
1216
+ are **blind** to a divergence — these are the only gates that catch one (the
1217
+ `cell_moderate` verb itself is manifest-covered). In-process hook coverage is
1218
+ the standalone `auth/cell_create_authorize.db.test.ts` (these cross suites skip
1219
+ in-process, where the default app mounts no authorizer).
1220
+
1221
+ `cell_crud` / `cell_relations` gate `true` on TS + Rust (cells run on both, no
1222
+ `.skip`); `cell_gated_create` is cross-only (the policy is a spine-binary
1223
+ fixture). Cross-process wiring is `src/test/cross_backend/cell.cross.test.ts`
1224
+ (all the cell suites); the in-process legs (plain `gro test`) for crud/relations
1225
+ are `src/test/auth/cell_crud_parity.db.test.ts`
1196
1226
 
1197
1227
  - `cell_relations_parity.db.test.ts`, sharing the full-surface
1198
1228
  `create_cell_parity_setup` (`cell_parity_helpers.ts`) which mounts every cell