@fuzdev/fuz_app 0.67.1 → 0.68.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 (164) hide show
  1. package/dist/auth/CLAUDE.md +99 -5
  2. package/dist/auth/account_queries.d.ts +87 -4
  3. package/dist/auth/account_queries.d.ts.map +1 -1
  4. package/dist/auth/account_queries.js +107 -17
  5. package/dist/auth/account_schema.d.ts +19 -0
  6. package/dist/auth/account_schema.d.ts.map +1 -1
  7. package/dist/auth/account_schema.js +8 -0
  8. package/dist/auth/admin_action_specs.d.ts +168 -0
  9. package/dist/auth/admin_action_specs.d.ts.map +1 -1
  10. package/dist/auth/admin_action_specs.js +146 -1
  11. package/dist/auth/admin_actions.d.ts.map +1 -1
  12. package/dist/auth/admin_actions.js +218 -4
  13. package/dist/auth/audit_log_ddl.d.ts +10 -1
  14. package/dist/auth/audit_log_ddl.d.ts.map +1 -1
  15. package/dist/auth/audit_log_ddl.js +13 -4
  16. package/dist/auth/audit_log_schema.d.ts +34 -1
  17. package/dist/auth/audit_log_schema.d.ts.map +1 -1
  18. package/dist/auth/audit_log_schema.js +73 -0
  19. package/dist/auth/auth_ddl.d.ts +2 -2
  20. package/dist/auth/auth_ddl.d.ts.map +1 -1
  21. package/dist/auth/auth_ddl.js +10 -2
  22. package/dist/auth/cell_action_specs.d.ts +1295 -0
  23. package/dist/auth/cell_action_specs.d.ts.map +1 -0
  24. package/dist/auth/cell_action_specs.js +397 -0
  25. package/dist/auth/cell_actions.d.ts +63 -0
  26. package/dist/auth/cell_actions.d.ts.map +1 -0
  27. package/dist/auth/cell_actions.js +546 -0
  28. package/dist/auth/cell_audit_action_specs.d.ts +131 -0
  29. package/dist/auth/cell_audit_action_specs.d.ts.map +1 -0
  30. package/dist/auth/cell_audit_action_specs.js +70 -0
  31. package/dist/auth/cell_audit_actions.d.ts +18 -0
  32. package/dist/auth/cell_audit_actions.d.ts.map +1 -0
  33. package/dist/auth/cell_audit_actions.js +59 -0
  34. package/dist/auth/cell_audit_events.d.ts +28 -0
  35. package/dist/auth/cell_audit_events.d.ts.map +1 -0
  36. package/dist/auth/cell_audit_events.js +42 -0
  37. package/dist/auth/cell_audit_metadata.d.ts +48 -0
  38. package/dist/auth/cell_audit_metadata.d.ts.map +1 -0
  39. package/dist/auth/cell_audit_metadata.js +46 -0
  40. package/dist/auth/cell_authorize.d.ts +88 -0
  41. package/dist/auth/cell_authorize.d.ts.map +1 -0
  42. package/dist/auth/cell_authorize.js +172 -0
  43. package/dist/auth/cell_data_schema.d.ts +44 -0
  44. package/dist/auth/cell_data_schema.d.ts.map +1 -0
  45. package/dist/auth/cell_data_schema.js +42 -0
  46. package/dist/auth/cell_field_action_specs.d.ts +244 -0
  47. package/dist/auth/cell_field_action_specs.d.ts.map +1 -0
  48. package/dist/auth/cell_field_action_specs.js +136 -0
  49. package/dist/auth/cell_field_actions.d.ts +34 -0
  50. package/dist/auth/cell_field_actions.d.ts.map +1 -0
  51. package/dist/auth/cell_field_actions.js +153 -0
  52. package/dist/auth/cell_field_audit_metadata.d.ts +30 -0
  53. package/dist/auth/cell_field_audit_metadata.d.ts.map +1 -0
  54. package/dist/auth/cell_field_audit_metadata.js +28 -0
  55. package/dist/auth/cell_grant_action_specs.d.ts +333 -0
  56. package/dist/auth/cell_grant_action_specs.d.ts.map +1 -0
  57. package/dist/auth/cell_grant_action_specs.js +148 -0
  58. package/dist/auth/cell_grant_actions.d.ts +50 -0
  59. package/dist/auth/cell_grant_actions.d.ts.map +1 -0
  60. package/dist/auth/cell_grant_actions.js +208 -0
  61. package/dist/auth/cell_grant_audit_metadata.d.ts +75 -0
  62. package/dist/auth/cell_grant_audit_metadata.d.ts.map +1 -0
  63. package/dist/auth/cell_grant_audit_metadata.js +54 -0
  64. package/dist/auth/cell_item_action_specs.d.ts +331 -0
  65. package/dist/auth/cell_item_action_specs.d.ts.map +1 -0
  66. package/dist/auth/cell_item_action_specs.js +182 -0
  67. package/dist/auth/cell_item_actions.d.ts +37 -0
  68. package/dist/auth/cell_item_actions.d.ts.map +1 -0
  69. package/dist/auth/cell_item_actions.js +204 -0
  70. package/dist/auth/cell_item_audit_metadata.d.ts +35 -0
  71. package/dist/auth/cell_item_audit_metadata.d.ts.map +1 -0
  72. package/dist/auth/cell_item_audit_metadata.js +32 -0
  73. package/dist/auth/cell_relation_visibility.d.ts +32 -0
  74. package/dist/auth/cell_relation_visibility.d.ts.map +1 -0
  75. package/dist/auth/cell_relation_visibility.js +57 -0
  76. package/dist/auth/deps.d.ts +9 -0
  77. package/dist/auth/deps.d.ts.map +1 -1
  78. package/dist/auth/role_grant_queries.d.ts +30 -0
  79. package/dist/auth/role_grant_queries.d.ts.map +1 -1
  80. package/dist/auth/role_grant_queries.js +54 -0
  81. package/dist/db/CLAUDE.md +118 -0
  82. package/dist/db/cell_audit_queries.d.ts +26 -0
  83. package/dist/db/cell_audit_queries.d.ts.map +1 -0
  84. package/dist/db/cell_audit_queries.js +53 -0
  85. package/dist/db/cell_ddl.d.ts +151 -0
  86. package/dist/db/cell_ddl.d.ts.map +1 -0
  87. package/dist/db/cell_ddl.js +247 -0
  88. package/dist/db/cell_field_queries.d.ts +105 -0
  89. package/dist/db/cell_field_queries.d.ts.map +1 -0
  90. package/dist/db/cell_field_queries.js +113 -0
  91. package/dist/db/cell_grant_queries.d.ts +132 -0
  92. package/dist/db/cell_grant_queries.d.ts.map +1 -0
  93. package/dist/db/cell_grant_queries.js +145 -0
  94. package/dist/db/cell_history_ddl.d.ts +38 -0
  95. package/dist/db/cell_history_ddl.d.ts.map +1 -0
  96. package/dist/db/cell_history_ddl.js +61 -0
  97. package/dist/db/cell_item_queries.d.ts +107 -0
  98. package/dist/db/cell_item_queries.d.ts.map +1 -0
  99. package/dist/db/cell_item_queries.js +119 -0
  100. package/dist/db/cell_queries.d.ts +327 -0
  101. package/dist/db/cell_queries.d.ts.map +1 -0
  102. package/dist/db/cell_queries.js +431 -0
  103. package/dist/db/fact_ddl.d.ts +38 -0
  104. package/dist/db/fact_ddl.d.ts.map +1 -0
  105. package/dist/db/fact_ddl.js +71 -0
  106. package/dist/db/fact_queries.d.ts +140 -0
  107. package/dist/db/fact_queries.d.ts.map +1 -0
  108. package/dist/db/fact_queries.js +161 -0
  109. package/dist/db/fact_store.d.ts +112 -0
  110. package/dist/db/fact_store.d.ts.map +1 -0
  111. package/dist/db/fact_store.js +225 -0
  112. package/dist/server/env.d.ts +2 -0
  113. package/dist/server/env.d.ts.map +1 -1
  114. package/dist/server/env.js +6 -0
  115. package/dist/server/fact_write.d.ts +32 -0
  116. package/dist/server/fact_write.d.ts.map +1 -0
  117. package/dist/server/fact_write.js +56 -0
  118. package/dist/server/file_fact_fetcher.d.ts +42 -0
  119. package/dist/server/file_fact_fetcher.d.ts.map +1 -0
  120. package/dist/server/file_fact_fetcher.js +60 -0
  121. package/dist/server/file_fact_url.d.ts +53 -0
  122. package/dist/server/file_fact_url.d.ts.map +1 -0
  123. package/dist/server/file_fact_url.js +52 -0
  124. package/dist/server/serve_fact_route.d.ts +78 -0
  125. package/dist/server/serve_fact_route.d.ts.map +1 -0
  126. package/dist/server/serve_fact_route.js +205 -0
  127. package/dist/testing/CLAUDE.md +58 -5
  128. package/dist/testing/app_server.d.ts +12 -0
  129. package/dist/testing/app_server.d.ts.map +1 -1
  130. package/dist/testing/app_server.js +36 -2
  131. package/dist/testing/audit_completeness.d.ts.map +1 -1
  132. package/dist/testing/audit_completeness.js +67 -1
  133. package/dist/testing/cross_backend/account_lifecycle.d.ts +10 -0
  134. package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -0
  135. package/dist/testing/cross_backend/account_lifecycle.js +76 -0
  136. package/dist/testing/cross_backend/capabilities.d.ts +31 -0
  137. package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
  138. package/dist/testing/cross_backend/capabilities.js +3 -0
  139. package/dist/testing/cross_backend/cell_cross_helpers.d.ts +39 -0
  140. package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +1 -0
  141. package/dist/testing/cross_backend/cell_cross_helpers.js +45 -0
  142. package/dist/testing/cross_backend/cell_crud.d.ts +4 -0
  143. package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -0
  144. package/dist/testing/cross_backend/cell_crud.js +168 -0
  145. package/dist/testing/cross_backend/cell_relations.d.ts +4 -0
  146. package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -0
  147. package/dist/testing/cross_backend/cell_relations.js +229 -0
  148. package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
  149. package/dist/testing/cross_backend/default_backend_configs.js +6 -0
  150. package/dist/testing/cross_backend/setup.d.ts.map +1 -1
  151. package/dist/testing/cross_backend/setup.js +5 -0
  152. package/dist/testing/entities.d.ts.map +1 -1
  153. package/dist/testing/entities.js +4 -0
  154. package/dist/testing/ws_round_trip.d.ts.map +1 -1
  155. package/dist/testing/ws_round_trip.js +4 -0
  156. package/dist/ui/AdminAccounts.svelte +58 -0
  157. package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
  158. package/dist/ui/admin_accounts_state.svelte.d.ts +30 -2
  159. package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
  160. package/dist/ui/admin_accounts_state.svelte.js +45 -1
  161. package/dist/ui/admin_rpc_adapters.d.ts +6 -2
  162. package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
  163. package/dist/ui/admin_rpc_adapters.js +5 -1
  164. package/package.json +2 -2
@@ -0,0 +1,431 @@
1
+ /**
2
+ * Raw queries against the `cell` table.
3
+ *
4
+ * Convention: `deps: QueryDeps` first, no audit side effects, mutations
5
+ * return the affected row (or `null` for not-found).
6
+ *
7
+ * `cell.refs` is auto-extracted from `data` on every create and update via
8
+ * `fact_hash_extract_refs` (depth-first walk for `blake3:`-prefixed strings). Callers
9
+ * never pass `refs` directly — the column is a derived projection of
10
+ * `data` for cells-by-fact discovery, mirroring what a fact store does for
11
+ * JSON facts.
12
+ *
13
+ * Soft delete via `deleted_at`. All `get` / `list` queries exclude
14
+ * tombstones by default; `include_deleted: true` opts in for admin /
15
+ * audit views.
16
+ *
17
+ * `path` uniqueness is global, enforced by `idx_cell_path_unique` (partial
18
+ * on active rows). Path reuse after soft delete falls out of the partial
19
+ * index — queries do not need special handling.
20
+ *
21
+ * @module
22
+ */
23
+ import { fact_hash_extract_refs } from '@fuzdev/fuz_util/fact_hash.js';
24
+ import { assert_row } from './assert_row.js';
25
+ /**
26
+ * SQL fragment for the `grant_count` projection — correlated subquery
27
+ * against `cell_grant`. Inlined in every cell-row SELECT / RETURNING so
28
+ * `CellRow` carries the count uniformly. Pass the cell alias used in
29
+ * the outer query (`'cell'` for table-name references, `'c'` for the
30
+ * aliased form in `query_cell_list`).
31
+ *
32
+ * `::int` narrows from `bigint` to `int` so the JS row hydrates as a
33
+ * `number` rather than a bigint primitive — counts on a single cell are
34
+ * trivially within int32.
35
+ */
36
+ const grant_count_projection = (cell_alias) => `(SELECT COUNT(*)::int FROM cell_grant WHERE cell_id = ${cell_alias}.id) AS grant_count`;
37
+ /**
38
+ * Insert a cell row, deriving `refs` from `data`.
39
+ *
40
+ * `updated_by` is left NULL on insert — same convention as `updated_at`
41
+ * (NULL until first update). The "last modifier" stamp is meaningful only
42
+ * after a real edit; copying the creator's id into `updated_by` at create
43
+ * time would make a no-op update by a different actor look authored by
44
+ * the creator.
45
+ *
46
+ * @param deps - query deps
47
+ * @param input - data, optional visibility, path, and ownership
48
+ * @returns the inserted row
49
+ * @mutates `cell` - inserts one row
50
+ */
51
+ export const query_cell_create = async (deps, input) => {
52
+ const refs = derive_refs(input.data);
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)
56
+ RETURNING *, ${grant_count_projection('cell')}`, [
57
+ JSON.stringify(input.data),
58
+ input.visibility ?? null,
59
+ input.path ?? null,
60
+ refs,
61
+ input.created_by ?? null,
62
+ ]);
63
+ return assert_row(row, 'INSERT INTO cell');
64
+ };
65
+ /**
66
+ * Fetch a cell by id. Excludes soft-deleted rows by default.
67
+ *
68
+ * @param deps - query deps
69
+ * @param id - cell id
70
+ * @param options - `include_deleted: true` returns tombstones
71
+ * @returns the row or `null` when not found (or soft-deleted and not requested)
72
+ */
73
+ export const query_cell_get = async (deps, id, options) => {
74
+ const include_deleted = options?.include_deleted === true;
75
+ const row = await deps.db.query_one(`SELECT *, ${grant_count_projection('cell')}
76
+ FROM cell
77
+ WHERE id = $1
78
+ AND ($2::bool OR deleted_at IS NULL)`, [id, include_deleted]);
79
+ return row ?? null;
80
+ };
81
+ /**
82
+ * Fetch a cell by `path`. Excludes soft-deleted rows; the global partial
83
+ * unique index on `path WHERE deleted_at IS NULL` guarantees at most one
84
+ * result.
85
+ *
86
+ * @param deps - query deps
87
+ * @param path - the named lookup alias (e.g. `/map/main`)
88
+ * @returns the row or `null` when not found
89
+ */
90
+ export const query_cell_get_by_path = async (deps, path) => {
91
+ const row = await deps.db.query_one(`SELECT *, ${grant_count_projection('cell')}
92
+ FROM cell
93
+ WHERE path = $1 AND deleted_at IS NULL`, [path]);
94
+ return row ?? null;
95
+ };
96
+ /**
97
+ * Bulk-load active cell rows by id, **no visibility filter applied**. Used
98
+ * by the strict relation-read filter (`auth/cell_relation_visibility.ts`'s
99
+ * `filter_visible_target_ids`), which runs `can_view_cell` per row in
100
+ * memory rather than in SQL. Soft-deleted rows are excluded so relations
101
+ * to tombstones never surface.
102
+ *
103
+ * @param deps - query deps
104
+ * @param ids - cell ids to load (duplicates are harmless)
105
+ * @returns active rows in arbitrary order (caller indexes by `id`)
106
+ */
107
+ export const query_cell_load_many = async (deps, ids) => {
108
+ if (ids.length === 0)
109
+ return [];
110
+ return deps.db.query(`SELECT *, ${grant_count_projection('cell')}
111
+ FROM cell
112
+ WHERE id = ANY($1::uuid[]) AND deleted_at IS NULL`, [ids]);
113
+ };
114
+ /**
115
+ * Update a cell. Fields left `undefined` in the patch keep their existing
116
+ * value; explicit `null` writes `NULL`. `refs` is re-derived from `data`
117
+ * whenever the patch updates `data`. `updated_at` is bumped to `NOW()`
118
+ * on every successful update.
119
+ *
120
+ * @param deps - query deps
121
+ * @param id - cell id
122
+ * @param patch - subset of mutable fields
123
+ * @returns the updated row, or `null` when no row matched (already deleted
124
+ * or never existed)
125
+ * @mutates `cell` - updates one row
126
+ */
127
+ export const query_cell_update = async (deps, id, patch) => {
128
+ const data_provided = patch.data !== undefined;
129
+ const refs = data_provided ? derive_refs(patch.data) : null;
130
+ const visibility_provided = patch.visibility !== undefined;
131
+ const row = await deps.db.query_one(`UPDATE cell SET
132
+ data = CASE WHEN $2::bool THEN $3::jsonb ELSE data END,
133
+ refs = CASE WHEN $2::bool THEN $4::text[] ELSE refs END,
134
+ path = CASE WHEN $5::bool THEN $6 ELSE path END,
135
+ updated_by = CASE WHEN $7::bool THEN $8 ELSE updated_by END,
136
+ visibility = CASE WHEN $9::bool THEN $10::cell_visibility ELSE visibility END,
137
+ updated_at = NOW()
138
+ WHERE id = $1 AND deleted_at IS NULL
139
+ RETURNING *, ${grant_count_projection('cell')}`, [
140
+ id,
141
+ data_provided,
142
+ data_provided ? JSON.stringify(patch.data) : null,
143
+ refs,
144
+ patch.path !== undefined,
145
+ patch.path ?? null,
146
+ patch.updated_by !== undefined,
147
+ patch.updated_by ?? null,
148
+ visibility_provided,
149
+ patch.visibility ?? null,
150
+ ]);
151
+ return row ?? null;
152
+ };
153
+ /**
154
+ * Soft-delete a cell. Sets `deleted_at = NOW()`, `updated_at = NOW()`,
155
+ * and `updated_by = options.deleted_by` (or `NULL`). No-op when the row
156
+ * is already deleted.
157
+ *
158
+ * @param deps - query deps
159
+ * @param id - cell id
160
+ * @param options - `deleted_by` records who triggered the delete
161
+ * @returns `true` when a row was soft-deleted, `false` when no active row matched
162
+ * @mutates `cell` - sets `deleted_at` on one row
163
+ */
164
+ export const query_cell_delete = async (deps, id, options) => {
165
+ const row = await deps.db.query_one(`UPDATE cell
166
+ SET deleted_at = NOW(),
167
+ updated_at = NOW(),
168
+ updated_by = $2
169
+ WHERE id = $1 AND deleted_at IS NULL
170
+ RETURNING id`, [id, options?.deleted_by ?? null]);
171
+ return row !== undefined;
172
+ };
173
+ /**
174
+ * List cells whose `data.kind` matches the given value, newest first.
175
+ * Uses the `idx_cell_data` GIN index (`data @> ...`).
176
+ *
177
+ * @param deps - query deps
178
+ * @param kind - `data.kind` value to match (e.g. `'collection'`, `'entry'`)
179
+ * @param options - pagination
180
+ * @returns matching active rows
181
+ */
182
+ export const query_cell_list_by_data_kind = async (deps, kind, options) => deps.db.query(`SELECT *, ${grant_count_projection('cell')}
183
+ FROM cell
184
+ WHERE data @> $1::jsonb
185
+ AND deleted_at IS NULL
186
+ ORDER BY created_at DESC
187
+ LIMIT $2 OFFSET $3`, [JSON.stringify({ kind }), options?.limit ?? null, options?.offset ?? 0]);
188
+ /**
189
+ * List active cells created by an actor, newest first. Backed by the
190
+ * `idx_cell_created_by` partial index.
191
+ *
192
+ * @param deps - query deps
193
+ * @param actor_id - the creator's actor id
194
+ * @param options - pagination
195
+ * @returns matching active rows
196
+ */
197
+ export const query_cell_list_by_creator = async (deps, actor_id, options) => deps.db.query(`SELECT *, ${grant_count_projection('cell')}
198
+ FROM cell
199
+ WHERE created_by = $1 AND deleted_at IS NULL
200
+ ORDER BY created_at DESC
201
+ LIMIT $2 OFFSET $3`, [actor_id, options?.limit ?? null, options?.offset ?? 0]);
202
+ /**
203
+ * Filterable list query for the generic `cell_list` RPC.
204
+ *
205
+ * Takes a flat filter shape (single optional clause per dimension; the
206
+ * `cell_list` API explicitly does NOT support OR'd alternatives within a
207
+ * dimension — keep it simple) plus an optional viewer-aware visibility
208
+ * predicate.
209
+ *
210
+ * The visibility predicate mirrors `can_view_cell` in SQL form:
211
+ *
212
+ * ```
213
+ * (viewer_is_admin
214
+ * OR cell.visibility = 'public'
215
+ * OR (viewer_actor_id IS NOT NULL AND created_by = viewer_actor_id)
216
+ * OR (viewer_actor_id IS NOT NULL AND <grant admits caller>))
217
+ * ```
218
+ *
219
+ * The grants branch closes parity with `can_view_cell`: a SQL `EXISTS`
220
+ * over `cell_grant`, parameterized by the caller's `actor_id` and the
221
+ * parallel `(role[], scope_id[])` projection of `auth.role_grants`. The
222
+ * caller's role_grants are materialized once via a `caller_role_grants`
223
+ * CTE so the role-grant `unnest` isn't re-scanned per outer row. Empty
224
+ * role_grant arrays are fine: the CTE yields zero rows, the inner EXISTS
225
+ * returns false, and the actor-grant branch still fires for actor-shaped
226
+ * grants.
227
+ *
228
+ * `shared_with_caller_only: true` (`shared_with: 'me'` at the wire layer)
229
+ * takes a **different SQL shape**: instead of layering an extra
230
+ * conjunction on the cell-driven scan, it semi-joins through
231
+ * `cell_grant`, letting the planner drive from the (typically tiny)
232
+ * admitted-grant set via `idx_cell_grant_actor` /
233
+ * `idx_cell_grant_role_scope` rather than scanning every cell row. For a
234
+ * sharee with N grants over a table of M cells, the cost drops from
235
+ * O(M) to O(N + matched-cells). Owner-is-implicit (a cell's owner never
236
+ * appears as a grant principal) means the grants branch is itself
237
+ * owner-excluding, but the explicit `created_by IS DISTINCT FROM caller`
238
+ * guards against any future deviation. The shared_with branch does NOT
239
+ * bypass for admin: an admin asking "what's shared with me" wants their
240
+ * own grant footprint, not every cell.
241
+ *
242
+ * Soft-deleted rows are excluded by default; opt-in via `include_deleted`.
243
+ *
244
+ * @param deps - query deps
245
+ * @param params - filter + visibility + ordering + pagination
246
+ * @returns matching rows, ordered per `order_by` / `order_direction`
247
+ */
248
+ export const query_cell_list = async (deps, params) => {
249
+ const order_column = params.order_by === 'updated_at' ? 'updated_at' : 'created_at';
250
+ const order_direction = params.order_direction === 'asc' ? 'ASC' : 'DESC';
251
+ // Caller's actor + role_grants feed the `cell_grant` predicate. Empty
252
+ // arrays (not NULL) keep the SQL uniform — the `caller_role_grants` CTE
253
+ // yields zero rows for an empty role_grant set, no special-casing needed.
254
+ const caller_actor_id = params.caller_actor_id ?? null;
255
+ const role_grant_roles = params.caller_role_grant_roles ?? [];
256
+ const role_grant_scope_ids = params.caller_role_grant_scope_ids ?? [];
257
+ // Parallel-array invariant. `unnest(text[], uuid[])` null-pads on
258
+ // length mismatch — a longer roles array would silently widen
259
+ // role-grant admits with NULL `scope_id`s (treated as any-scope by
260
+ // the predicate). Security-relevant; assert at the SQL boundary
261
+ // rather than trusting every caller.
262
+ if (role_grant_roles.length !== role_grant_scope_ids.length) {
263
+ throw new Error(`query_cell_list: caller_role_grant_roles (len=${role_grant_roles.length}) and ` +
264
+ `caller_role_grant_scope_ids (len=${role_grant_scope_ids.length}) must be parallel arrays`);
265
+ }
266
+ const shared_with_caller_only = params.shared_with_caller_only === true;
267
+ // Column references and `$N::type` placeholders are interpolated; user
268
+ // values flow exclusively through the parameterized array below.
269
+ //
270
+ // `starts_with(path, $5)` is used instead of `LIKE $5 || '%'` so caller-
271
+ // supplied wildcards (`%`, `_`, `\`) match literally — Postgres 11+ has
272
+ // the function and pglite/PG 16 supports it.
273
+ //
274
+ // Two SQL shapes share the same 14-param positional layout:
275
+ //
276
+ // - `shared_with_caller_only: false` — cell-driven scan with the
277
+ // visibility predicate (admin / public / owner / grant-admits).
278
+ // - `shared_with_caller_only: true` — grant-driven semi-join: the
279
+ // planner walks `cell_grant` first via partial indexes
280
+ // (`idx_cell_grant_actor`, `idx_cell_grant_role_scope`) and probes
281
+ // `cell` by id, instead of scanning every row in `cell`.
282
+ const sql = shared_with_caller_only
283
+ ? build_shared_with_sql(order_column, order_direction)
284
+ : build_general_sql(order_column, order_direction);
285
+ return deps.db.query(sql, [
286
+ params.include_deleted === true,
287
+ params.data_kind ?? null,
288
+ params.ref ?? null,
289
+ params.created_by ?? null,
290
+ params.path_prefix ?? null,
291
+ params.viewer_is_admin,
292
+ params.viewer_actor_id ?? null,
293
+ params.limit ?? null,
294
+ params.offset ?? 0,
295
+ params.ids && params.ids.length > 0 ? params.ids : null,
296
+ caller_actor_id,
297
+ role_grant_roles,
298
+ role_grant_scope_ids,
299
+ params.visibility ?? null,
300
+ ]);
301
+ };
302
+ /**
303
+ * The `cell_grant` admits-caller predicate, factored once and reused by
304
+ * both SQL shapes (general visibility branch + shared-with-me semi-join).
305
+ *
306
+ * Resolves true when the grant row at `g_alias` admits the caller via
307
+ * either an actor-shaped principal (`g.actor_id = $11`) or a
308
+ * role-shaped principal whose `(role, scope_id)` matches a row in the
309
+ * `caller_role_grants` CTE. NULL `g.scope_id` matches any scope, mirroring
310
+ * `grant_admits` in `cell_authorize.ts`.
311
+ */
312
+ const grant_admits_caller_predicate = (g_alias) => `(
313
+ ($11::uuid IS NOT NULL AND ${g_alias}.actor_id = $11)
314
+ OR (
315
+ ${g_alias}.role IS NOT NULL
316
+ AND EXISTS (
317
+ SELECT 1 FROM caller_role_grants p
318
+ WHERE p.role = ${g_alias}.role
319
+ AND (${g_alias}.scope_id IS NULL OR p.scope_id IS NOT DISTINCT FROM ${g_alias}.scope_id)
320
+ )
321
+ )
322
+ )`;
323
+ /**
324
+ * Materialize the caller's `(role, scope_id)` role_grant pairs once per
325
+ * query so the role-grant predicate doesn't re-scan `unnest()` per
326
+ * outer row. PG 12+ inlines non-recursive single-use CTEs, so this is
327
+ * planner-equivalent to a subquery — the form is for clarity.
328
+ */
329
+ const CALLER_ROLE_GRANTS_CTE = `WITH caller_role_grants AS (
330
+ SELECT role, scope_id FROM unnest($12::text[], $13::uuid[]) AS p(role, scope_id)
331
+ )`;
332
+ /** General `cell_list` SQL: cell-driven scan, full visibility predicate. */
333
+ const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
334
+ SELECT c.*, ${grant_count_projection('c')} FROM cell c
335
+ WHERE ($1::bool OR c.deleted_at IS NULL)
336
+ AND ($2::text IS NULL OR c.data @> jsonb_build_object('kind', $2::text))
337
+ AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
338
+ AND ($3::text IS NULL OR c.refs @> ARRAY[$3]::text[])
339
+ AND ($4::uuid IS NULL OR c.created_by = $4)
340
+ AND ($5::text IS NULL OR starts_with(c.path, $5))
341
+ AND ($10::uuid[] IS NULL OR c.id = ANY($10))
342
+ AND (
343
+ $6::bool
344
+ OR c.visibility = 'public'
345
+ OR ($7::uuid IS NOT NULL AND c.created_by = $7)
346
+ OR ($7::uuid IS NOT NULL AND EXISTS (
347
+ SELECT 1 FROM cell_grant g
348
+ WHERE g.cell_id = c.id AND ${grant_admits_caller_predicate('g')}
349
+ ))
350
+ )
351
+ ORDER BY c.${order_column} ${order_direction} NULLS LAST
352
+ LIMIT $8 OFFSET $9`;
353
+ /**
354
+ * Shared-with-me `cell_list` SQL: grant-driven semi-join. The `IN
355
+ * (SELECT g.cell_id ...)` form lets the planner walk `cell_grant`
356
+ * first via the partial indexes on `actor_id` / `(role, scope_id)`,
357
+ * then probe `cell` by primary key. For a sharee with a few grants
358
+ * over a large `cell` table this is dramatically faster than the
359
+ * cell-driven `EXISTS` form.
360
+ *
361
+ * `$7::uuid IS NOT NULL` is asserted by the handler (anonymous callers
362
+ * are rejected at the action layer), but we re-encode it as
363
+ * `created_by IS DISTINCT FROM $7` so a NULL caller actor (defense in
364
+ * depth) doesn't accidentally admit cells with NULL `created_by`.
365
+ *
366
+ * `$6` (admin bypass) is intentionally not consulted: an admin asking
367
+ * "what's shared with me" wants their grant footprint, not every cell.
368
+ */
369
+ const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
370
+ SELECT c.*, ${grant_count_projection('c')} FROM cell c
371
+ WHERE ($1::bool OR c.deleted_at IS NULL)
372
+ AND ($2::text IS NULL OR c.data @> jsonb_build_object('kind', $2::text))
373
+ AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
374
+ AND ($3::text IS NULL OR c.refs @> ARRAY[$3]::text[])
375
+ AND ($4::uuid IS NULL OR c.created_by = $4)
376
+ AND ($5::text IS NULL OR starts_with(c.path, $5))
377
+ AND ($10::uuid[] IS NULL OR c.id = ANY($10))
378
+ AND $7::uuid IS NOT NULL
379
+ AND c.created_by IS DISTINCT FROM $7
380
+ AND c.id IN (
381
+ SELECT g.cell_id FROM cell_grant g
382
+ WHERE ${grant_admits_caller_predicate('g')}
383
+ )
384
+ -- $6 (viewer_is_admin) intentionally not consulted: an admin
385
+ -- asking "what's shared with me" wants their grant footprint, not
386
+ -- every cell. Cast-only reference keeps the param's type known to
387
+ -- the planner so the shared 14-param positional layout stays valid.
388
+ AND $6::bool IS NOT NULL
389
+ ORDER BY c.${order_column} ${order_direction} NULLS LAST
390
+ LIMIT $8 OFFSET $9`;
391
+ /**
392
+ * List active cells whose `refs` array contains the given fact hash,
393
+ * newest first. Backed by the `idx_cell_refs` GIN index.
394
+ *
395
+ * Used by the fact-serving route's authz walk: a fact is viewable iff
396
+ * **at least one** referencing active cell admits the caller via
397
+ * `can_view_cell`. Unreferenced facts (no row returned here) are
398
+ * unreachable through the public surface — orphan-fact GC handles them.
399
+ *
400
+ * `include_grant_count` defaults to true so the row hydrates uniformly
401
+ * with the rest of the cell query surface. The fact-serving route is
402
+ * the one hot path where the count is wasted work — pass `false`
403
+ * there to skip the per-row correlated subquery; the field falls back
404
+ * to a constant 0 so `CellRow` stays type-stable.
405
+ *
406
+ * @param deps - query deps
407
+ * @param hash - fact hash to search for
408
+ * @param options - pagination + grant-count toggle
409
+ * @returns matching active rows
410
+ */
411
+ export const query_cell_list_by_ref = async (deps, hash, options) => {
412
+ const include_grant_count = options?.include_grant_count !== false;
413
+ const projection = include_grant_count ? grant_count_projection('cell') : '0::int AS grant_count';
414
+ return deps.db.query(`SELECT *, ${projection}
415
+ FROM cell
416
+ WHERE refs @> ARRAY[$1]::text[]
417
+ AND deleted_at IS NULL
418
+ ORDER BY created_at DESC
419
+ LIMIT $2 OFFSET $3`, [hash, options?.limit ?? null, options?.offset ?? 0]);
420
+ };
421
+ /**
422
+ * Derive the `refs` array column value from a cell's `data`.
423
+ *
424
+ * Returns `null` (rather than `[]`) when no refs are present — the column
425
+ * is nullable and the `idx_cell_refs` partial index is `WHERE refs IS NOT
426
+ * NULL`, so an empty array would force every cell into the index.
427
+ */
428
+ const derive_refs = (data) => {
429
+ const refs = fact_hash_extract_refs(data);
430
+ return refs.length > 0 ? refs : null;
431
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Fact + memo PG schema.
3
+ *
4
+ * Three tables:
5
+ *
6
+ * - `facts` — content-addressed bytes. `hash = 'blake3:<hex64>'`. Either
7
+ * embedded (`bytes`) or referenced (`external_url`); the CHECK constraint
8
+ * enforces exactly one populated. Idempotent: same bytes always produce
9
+ * the same hash, so `INSERT … ON CONFLICT DO NOTHING` is the put primitive.
10
+ * - `fact_refs` — declared dependency edges (source fact → target fact).
11
+ * `target_hash` is intentionally **not** a foreign key: in federation a
12
+ * reference may target a fact stored on another instance.
13
+ * - `memos` — `(fn_id, input_hash) → output_hash` for memoized computations.
14
+ *
15
+ * @module
16
+ */
17
+ import type { Migration, MigrationNamespace } from './migrate.js';
18
+ /** `facts` table — content-addressed byte store. */
19
+ export declare const FACTS_SCHEMA = "\nCREATE TABLE IF NOT EXISTS facts (\n\thash TEXT PRIMARY KEY,\n\tbytes BYTEA,\n\texternal_url TEXT,\n\tcontent_type TEXT,\n\tsize BIGINT NOT NULL,\n\tcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n\tCONSTRAINT facts_storage_present CHECK (bytes IS NOT NULL OR external_url IS NOT NULL)\n)";
20
+ /**
21
+ * `fact_refs` table — declared dependency edges between facts.
22
+ *
23
+ * `target_hash` is not a foreign key (federation: target may live remotely).
24
+ */
25
+ export declare const FACT_REFS_SCHEMA = "\nCREATE TABLE IF NOT EXISTS fact_refs (\n\tsource_hash TEXT NOT NULL REFERENCES facts(hash) ON DELETE CASCADE,\n\ttarget_hash TEXT NOT NULL,\n\tPRIMARY KEY (source_hash, target_hash)\n)";
26
+ /** Reverse lookup: which facts reference a given target? */
27
+ export declare const FACT_REFS_TARGET_INDEX = "\nCREATE INDEX IF NOT EXISTS idx_fact_refs_target ON fact_refs(target_hash)";
28
+ /** `memos` table — `(fn_id, input_hash) → output_hash` for memoized computations. */
29
+ export declare const MEMOS_SCHEMA = "\nCREATE TABLE IF NOT EXISTS memos (\n\tfn_id TEXT NOT NULL,\n\tinput_hash TEXT NOT NULL,\n\toutput_hash TEXT NOT NULL,\n\tcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n\tPRIMARY KEY (fn_id, input_hash)\n)";
30
+ /** Tables created by `FACT_MIGRATION_NS`, in drop order (children first). */
31
+ export declare const FACT_DROP_TABLES: readonly ["memos", "fact_refs", "facts"];
32
+ /** Fact + memo migrations. */
33
+ export declare const FACT_MIGRATIONS: Array<Migration>;
34
+ /** Namespace identifier for fact + memo migrations. */
35
+ export declare const FACT_MIGRATION_NAMESPACE = "fuz_facts";
36
+ /** Migration namespace consumed by `run_migrations`. */
37
+ export declare const FACT_MIGRATION_NS: MigrationNamespace;
38
+ //# sourceMappingURL=fact_ddl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fact_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAGH,OAAO,KAAK,EAAC,SAAS,EAAE,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAEhE,oDAAoD;AACpD,eAAO,MAAM,YAAY,uSASvB,CAAC;AAEH;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,+LAK3B,CAAC;AAEH,4DAA4D;AAC5D,eAAO,MAAM,sBAAsB,gFACuC,CAAC;AAE3E,qFAAqF;AACrF,eAAO,MAAM,YAAY,oNAOvB,CAAC;AAEH,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,0CAA2C,CAAC;AAEzE,8BAA8B;AAC9B,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAU5C,CAAC;AAEF,uDAAuD;AACvD,eAAO,MAAM,wBAAwB,cAAc,CAAC;AAEpD,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Fact + memo PG schema.
3
+ *
4
+ * Three tables:
5
+ *
6
+ * - `facts` — content-addressed bytes. `hash = 'blake3:<hex64>'`. Either
7
+ * embedded (`bytes`) or referenced (`external_url`); the CHECK constraint
8
+ * enforces exactly one populated. Idempotent: same bytes always produce
9
+ * the same hash, so `INSERT … ON CONFLICT DO NOTHING` is the put primitive.
10
+ * - `fact_refs` — declared dependency edges (source fact → target fact).
11
+ * `target_hash` is intentionally **not** a foreign key: in federation a
12
+ * reference may target a fact stored on another instance.
13
+ * - `memos` — `(fn_id, input_hash) → output_hash` for memoized computations.
14
+ *
15
+ * @module
16
+ */
17
+ /** `facts` table — content-addressed byte store. */
18
+ export const FACTS_SCHEMA = `
19
+ CREATE TABLE IF NOT EXISTS facts (
20
+ hash TEXT PRIMARY KEY,
21
+ bytes BYTEA,
22
+ external_url TEXT,
23
+ content_type TEXT,
24
+ size BIGINT NOT NULL,
25
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
26
+ CONSTRAINT facts_storage_present CHECK (bytes IS NOT NULL OR external_url IS NOT NULL)
27
+ )`;
28
+ /**
29
+ * `fact_refs` table — declared dependency edges between facts.
30
+ *
31
+ * `target_hash` is not a foreign key (federation: target may live remotely).
32
+ */
33
+ export const FACT_REFS_SCHEMA = `
34
+ CREATE TABLE IF NOT EXISTS fact_refs (
35
+ source_hash TEXT NOT NULL REFERENCES facts(hash) ON DELETE CASCADE,
36
+ target_hash TEXT NOT NULL,
37
+ PRIMARY KEY (source_hash, target_hash)
38
+ )`;
39
+ /** Reverse lookup: which facts reference a given target? */
40
+ export const FACT_REFS_TARGET_INDEX = `
41
+ CREATE INDEX IF NOT EXISTS idx_fact_refs_target ON fact_refs(target_hash)`;
42
+ /** `memos` table — `(fn_id, input_hash) → output_hash` for memoized computations. */
43
+ export const MEMOS_SCHEMA = `
44
+ CREATE TABLE IF NOT EXISTS memos (
45
+ fn_id TEXT NOT NULL,
46
+ input_hash TEXT NOT NULL,
47
+ output_hash TEXT NOT NULL,
48
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
49
+ PRIMARY KEY (fn_id, input_hash)
50
+ )`;
51
+ /** Tables created by `FACT_MIGRATION_NS`, in drop order (children first). */
52
+ export const FACT_DROP_TABLES = ['memos', 'fact_refs', 'facts'];
53
+ /** Fact + memo migrations. */
54
+ export const FACT_MIGRATIONS = [
55
+ {
56
+ name: 'facts_v0',
57
+ up: async (db) => {
58
+ await db.query(FACTS_SCHEMA);
59
+ await db.query(FACT_REFS_SCHEMA);
60
+ await db.query(FACT_REFS_TARGET_INDEX);
61
+ await db.query(MEMOS_SCHEMA);
62
+ },
63
+ },
64
+ ];
65
+ /** Namespace identifier for fact + memo migrations. */
66
+ export const FACT_MIGRATION_NAMESPACE = 'fuz_facts';
67
+ /** Migration namespace consumed by `run_migrations`. */
68
+ export const FACT_MIGRATION_NS = {
69
+ namespace: FACT_MIGRATION_NAMESPACE,
70
+ migrations: FACT_MIGRATIONS,
71
+ };
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Raw queries against the `facts` and `fact_refs` tables.
3
+ *
4
+ * Convention: `deps: QueryDeps` first, no audit side effects, mutations are
5
+ * idempotent (`ON CONFLICT DO NOTHING`) so the same hash can be written by
6
+ * two callers without the second observing an error.
7
+ *
8
+ * Higher-level lifecycle (verify-on-read, JSON ref auto-extraction,
9
+ * embedded-vs-referenced selection) lives in `db/fact_store.ts`. Queries
10
+ * here are deliberately mechanical.
11
+ *
12
+ * @module
13
+ */
14
+ import type { QueryDeps } from './query_deps.js';
15
+ import type { FactHash } from '@fuzdev/fuz_util/fact_hash.js';
16
+ /** Row shape for `SELECT … FROM facts`. */
17
+ export interface FactRow {
18
+ hash: FactHash;
19
+ bytes: Uint8Array | null;
20
+ external_url: string | null;
21
+ content_type: string | null;
22
+ size: number | string;
23
+ created_at: Date;
24
+ }
25
+ /** Subset returned by metadata-only queries (no `bytes` payload). */
26
+ export interface FactMetaRow {
27
+ hash: FactHash;
28
+ external_url: string | null;
29
+ content_type: string | null;
30
+ size: number | string;
31
+ created_at: Date;
32
+ }
33
+ /**
34
+ * Idempotently insert a fact row.
35
+ *
36
+ * `bytes` xor `external_url` per the `facts_storage_present` CHECK
37
+ * constraint; the caller is responsible for satisfying it (the queries
38
+ * layer does not second-guess). Returns `true` when a new row was
39
+ * inserted, `false` when a row already existed (caller can use this to
40
+ * decide whether to also write `fact_refs`).
41
+ */
42
+ export declare const query_put_fact: (deps: QueryDeps, input: {
43
+ hash: FactHash;
44
+ bytes: Uint8Array | null;
45
+ external_url: string | null;
46
+ content_type: string | null;
47
+ size: number;
48
+ }) => Promise<boolean>;
49
+ /**
50
+ * Idempotently insert declared refs for a fact. No-ops on `(source_hash,
51
+ * target_hash)` collisions and skips the round trip entirely when
52
+ * `target_hashes` is empty.
53
+ */
54
+ export declare const query_put_fact_refs: (deps: QueryDeps, source_hash: FactHash, target_hashes: Array<FactHash>) => Promise<void>;
55
+ /**
56
+ * Fetch a fact's full row (including embedded `bytes`). Use this from
57
+ * `FactStore.get`; cheaper accessors live below.
58
+ */
59
+ export declare const query_get_fact: (deps: QueryDeps, hash: FactHash) => Promise<FactRow | null>;
60
+ /**
61
+ * Fetch metadata only — skips the (potentially large) `bytes` column.
62
+ */
63
+ export declare const query_get_fact_meta: (deps: QueryDeps, hash: FactHash) => Promise<FactMetaRow | null>;
64
+ /**
65
+ * Cheap existence check. Backed by the `facts` PK index.
66
+ */
67
+ export declare const query_has_fact: (deps: QueryDeps, hash: FactHash) => Promise<boolean>;
68
+ /**
69
+ * List declared targets for a source fact. Order is unspecified; callers
70
+ * that need stable ordering should sort.
71
+ */
72
+ export declare const query_get_fact_refs: (deps: QueryDeps, source_hash: FactHash) => Promise<Array<FactHash>>;
73
+ /**
74
+ * Drop a fact row. Cascades `fact_refs` rows via the `ON DELETE CASCADE`
75
+ * FK on `source_hash`. Returns the deleted row's `(size, external_url)`
76
+ * so the caller can unlink the disk file (if any) and tally freed bytes,
77
+ * or `null` when no row matched (idempotent: deleting an absent fact is
78
+ * not an error).
79
+ *
80
+ * NOTE: this is a low-level primitive — callers MUST verify the fact is
81
+ * truly orphan (no referencing cell) before calling. The orphan check
82
+ * lives in `query_orphan_facts_*` below; the lifecycle wrapper in
83
+ * `PgFactStore.delete` handles the disk-file unlink.
84
+ */
85
+ export declare const query_delete_fact: (deps: QueryDeps, hash: FactHash) => Promise<{
86
+ size: number;
87
+ external_url: string | null;
88
+ } | null>;
89
+ /**
90
+ * Summary + sample shape returned by `query_orphan_facts_list`. The sample
91
+ * is a small page (default 20 rows) shown in the admin panel so the
92
+ * operator has *some* visibility into what they're about to delete.
93
+ * Total `count` and `total_size_bytes` are over the full orphan set
94
+ * (matching the same predicate the delete handler will run).
95
+ */
96
+ export interface OrphanFactsListResult {
97
+ count: number;
98
+ total_size_bytes: number;
99
+ sample: Array<{
100
+ hash: FactHash;
101
+ size: number;
102
+ created_at: string;
103
+ external_url: string | null;
104
+ }>;
105
+ }
106
+ /**
107
+ * Compute the "orphan facts" set: rows in `facts` where no active
108
+ * (non-tombstone) `cell.refs` array contains the hash.
109
+ *
110
+ * The `cell` join is deliberately app-coupled — `facts` lives in the
111
+ * `fuz_facts` namespace and `cell.refs` lives in `fuz_cell`, but the
112
+ * orphan predicate only makes sense in apps that route content through
113
+ * cells. When a non-cell fact consumer ever appears (signed memo
114
+ * outputs? external fact mirrors?) the predicate moves to a generic
115
+ * `fact_consumers` registry; today the cell layer is the only consumer.
116
+ *
117
+ * The `older_than` filter applies to `facts.created_at`. Pass `null`
118
+ * to skip the filter (used by the list-summary preview); the delete
119
+ * handler always passes a non-null cutoff (default 0, meaning "any
120
+ * orphan").
121
+ *
122
+ * @param deps - query deps
123
+ * @param older_than - filter to facts created before this Date (or null
124
+ * to skip)
125
+ * @param sample_limit - row cap for the returned `sample`
126
+ */
127
+ export declare const query_orphan_facts_list: (deps: QueryDeps, older_than: Date | null, sample_limit: number) => Promise<OrphanFactsListResult>;
128
+ /**
129
+ * Select the orphan-fact hashes for deletion. Returns the rows directly
130
+ * (no row-count limit) — callers iterate to unlink disk files. The
131
+ * `older_than` cutoff is required (non-null) here: bulk delete should
132
+ * always be operator-scoped to a time window. A "delete all" sweep
133
+ * passes a far-future cutoff, not `null`.
134
+ */
135
+ export declare const query_orphan_facts_select_for_delete: (deps: QueryDeps, older_than: Date) => Promise<Array<{
136
+ hash: FactHash;
137
+ size: number;
138
+ external_url: string | null;
139
+ }>>;
140
+ //# sourceMappingURL=fact_queries.d.ts.map