@substrat-run/kernel 0.137.0 → 0.139.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 (121) hide show
  1. package/dist/attachment-extractor.d.ts +9 -1
  2. package/dist/attachment-extractor.d.ts.map +1 -1
  3. package/dist/attachment-extractor.js +21 -1
  4. package/dist/attachment-extractor.js.map +1 -1
  5. package/dist/attachment-text.d.ts +64 -0
  6. package/dist/attachment-text.d.ts.map +1 -1
  7. package/dist/attachment-text.js +87 -16
  8. package/dist/attachment-text.js.map +1 -1
  9. package/dist/attribution.d.ts +36 -3
  10. package/dist/attribution.d.ts.map +1 -1
  11. package/dist/attribution.js +21 -8
  12. package/dist/attribution.js.map +1 -1
  13. package/dist/delivery-refusal.d.ts +21 -0
  14. package/dist/delivery-refusal.d.ts.map +1 -0
  15. package/dist/delivery-refusal.js +21 -0
  16. package/dist/delivery-refusal.js.map +1 -0
  17. package/dist/entity-state-reads.d.ts +43 -0
  18. package/dist/entity-state-reads.d.ts.map +1 -0
  19. package/dist/entity-state-reads.js +135 -0
  20. package/dist/entity-state-reads.js.map +1 -0
  21. package/dist/entity-state.d.ts +177 -0
  22. package/dist/entity-state.d.ts.map +1 -0
  23. package/dist/entity-state.js +345 -0
  24. package/dist/entity-state.js.map +1 -0
  25. package/dist/findings.d.ts +143 -0
  26. package/dist/findings.d.ts.map +1 -0
  27. package/dist/findings.js +463 -0
  28. package/dist/findings.js.map +1 -0
  29. package/dist/index.d.ts +30 -20
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +22 -14
  32. package/dist/index.js.map +1 -1
  33. package/dist/job-run.d.ts +309 -48
  34. package/dist/job-run.d.ts.map +1 -1
  35. package/dist/job-run.js +388 -80
  36. package/dist/job-run.js.map +1 -1
  37. package/dist/list-index.d.ts +25 -3
  38. package/dist/list-index.d.ts.map +1 -1
  39. package/dist/list-index.js +70 -26
  40. package/dist/list-index.js.map +1 -1
  41. package/dist/membership-executor.d.ts +109 -0
  42. package/dist/membership-executor.d.ts.map +1 -0
  43. package/dist/membership-executor.js +261 -0
  44. package/dist/membership-executor.js.map +1 -0
  45. package/dist/membership-fence.d.ts +57 -0
  46. package/dist/membership-fence.d.ts.map +1 -0
  47. package/dist/membership-fence.js +90 -0
  48. package/dist/membership-fence.js.map +1 -0
  49. package/dist/module-migrations.d.ts +6 -3
  50. package/dist/module-migrations.d.ts.map +1 -1
  51. package/dist/module-migrations.js +12 -5
  52. package/dist/module-migrations.js.map +1 -1
  53. package/dist/operation-series.d.ts +58 -0
  54. package/dist/operation-series.d.ts.map +1 -0
  55. package/dist/operation-series.js +63 -0
  56. package/dist/operation-series.js.map +1 -0
  57. package/dist/peer.d.ts +13 -8
  58. package/dist/peer.d.ts.map +1 -1
  59. package/dist/peer.js +13 -14
  60. package/dist/peer.js.map +1 -1
  61. package/dist/permission-eval.d.ts +80 -1
  62. package/dist/permission-eval.d.ts.map +1 -1
  63. package/dist/permission-eval.js +164 -18
  64. package/dist/permission-eval.js.map +1 -1
  65. package/dist/platform-sweep.d.ts +7 -1
  66. package/dist/platform-sweep.d.ts.map +1 -1
  67. package/dist/platform-sweep.js +12 -0
  68. package/dist/platform-sweep.js.map +1 -1
  69. package/dist/read-only-sql.d.ts +2 -26
  70. package/dist/read-only-sql.d.ts.map +1 -1
  71. package/dist/read-only-sql.js +13 -9
  72. package/dist/read-only-sql.js.map +1 -1
  73. package/dist/scope-copy.d.ts +18 -0
  74. package/dist/scope-copy.d.ts.map +1 -1
  75. package/dist/scope-copy.js +18 -0
  76. package/dist/scope-copy.js.map +1 -1
  77. package/dist/scope-host.d.ts +416 -13
  78. package/dist/scope-host.d.ts.map +1 -1
  79. package/dist/scope-host.js +51 -2
  80. package/dist/scope-host.js.map +1 -1
  81. package/dist/scope-role-admin.d.ts +68 -0
  82. package/dist/scope-role-admin.d.ts.map +1 -0
  83. package/dist/scope-role-admin.js +80 -0
  84. package/dist/scope-role-admin.js.map +1 -0
  85. package/dist/search-index.d.ts +13 -1
  86. package/dist/search-index.d.ts.map +1 -1
  87. package/dist/search-index.js +9 -16
  88. package/dist/search-index.js.map +1 -1
  89. package/dist/spine-guard.d.ts +64 -2
  90. package/dist/spine-guard.d.ts.map +1 -1
  91. package/dist/spine-guard.js +345 -10
  92. package/dist/spine-guard.js.map +1 -1
  93. package/dist/spine-restore.d.ts +7 -2
  94. package/dist/spine-restore.d.ts.map +1 -1
  95. package/dist/spine-restore.js +13 -5
  96. package/dist/spine-restore.js.map +1 -1
  97. package/dist/sql-identifier.d.ts +12 -0
  98. package/dist/sql-identifier.d.ts.map +1 -0
  99. package/dist/sql-identifier.js +17 -0
  100. package/dist/sql-identifier.js.map +1 -0
  101. package/dist/system-switch-record.d.ts +277 -77
  102. package/dist/system-switch-record.d.ts.map +1 -1
  103. package/dist/system-switch-record.js +322 -105
  104. package/dist/system-switch-record.js.map +1 -1
  105. package/dist/system-switch.d.ts +110 -14
  106. package/dist/system-switch.d.ts.map +1 -1
  107. package/dist/system-switch.js +92 -17
  108. package/dist/system-switch.js.map +1 -1
  109. package/dist/timeline.d.ts +23 -0
  110. package/dist/timeline.d.ts.map +1 -1
  111. package/dist/timeline.js +30 -0
  112. package/dist/timeline.js.map +1 -1
  113. package/dist/ulid.d.ts +5 -0
  114. package/dist/ulid.d.ts.map +1 -1
  115. package/dist/ulid.js +9 -0
  116. package/dist/ulid.js.map +1 -1
  117. package/dist/vertical-events.d.ts +7 -1
  118. package/dist/vertical-events.d.ts.map +1 -1
  119. package/dist/vertical-events.js +14 -0
  120. package/dist/vertical-events.js.map +1 -1
  121. package/package.json +4 -4
@@ -0,0 +1,345 @@
1
+ /**
2
+ * Entity archive and trash (#119): the kernel half, written once — both adapters hand
3
+ * `createEntityStateVerbs` the same things, the way `createEntityEdgeVerbs` is shared.
4
+ *
5
+ * ## What the kernel owns, and what it leaves to the vertical
6
+ *
7
+ * | Kernel | Vertical |
8
+ * |---|---|
9
+ * | The two columns on the entity's table, added by a derived migration | Which entities declare `archive` / `trash`, and the keys |
10
+ * | The transitions, their refusals, and the event each one emits | The operations that call the verbs, and their routes |
11
+ * | The declared key, re-checked on the entity by every verb and every trashed read | What an archived row looks like on a screen |
12
+ * | `ctx.page` / `ctx.search` leaving archived and trashed rows out by default | Its own hand-written `SELECT`s — see the gap below |
13
+ *
14
+ * ## The state is two columns
15
+ *
16
+ * `_substrat_archived_at` and `_substrat_trashed_at`, each an ISO instant or NULL. Apart, not
17
+ * one enum, so a trash never forgets an archive: `restore` clears the trash and leaves the
18
+ * archive, and an archived row that went to the bin comes back archived. The visible state is
19
+ * derived (contracts `entity-state.ts`): trashed, else archived, else active.
20
+ *
21
+ * Module code cannot write either column: `ctx.sql` refuses a write naming a `_substrat_*`
22
+ * column (`spine-guard.ts`), so the only way into or out of the trash is a verb that checks
23
+ * the key and emits the event. It can READ them, which is how a vertical's own `SELECT` keeps
24
+ * archived rows out of a screen `ctx.page` does not compose.
25
+ *
26
+ * ## The gap, stated
27
+ *
28
+ * The kernel filters the reads it composes — `ctx.page` and `ctx.search` — and nothing else.
29
+ * A handler's own `SELECT … FROM todo_lists WHERE id = ?` sees an archived or trashed row
30
+ * exactly as before. That is the honest limit of a kernel that does not parse module SQL:
31
+ * a get-by-id asks `ctx.entityState(ref)` and decides, and a hand-written list adds the
32
+ * predicate itself (`entityStateWhere`).
33
+ *
34
+ * ## Trash is not erasure, and not tenant deletion
35
+ *
36
+ * Trash keeps the row and every event about it. Subject erasure (#37) reaches a trashed row
37
+ * exactly as it reaches any other, because the row never moved tables; a tenant's deletion
38
+ * (#36) removes it with everything else. Nothing here extends a retention or shortens one.
39
+ */
40
+ import { ARCHIVED_AT_COLUMN, ENTITY_ARCHIVED, ENTITY_RESTORED, ENTITY_TRASHED, ENTITY_UNARCHIVED, TRASHED_AT_COLUMN, entityStateChangedPayload, substratError, } from '@substrat-run/contracts';
41
+ import { assertAllowed } from './permission-checker.js';
42
+ import { assertSqlIdentifier } from './sql-identifier.js';
43
+ /**
44
+ * Resolve one module's declarations. Refuses rather than skips, for `searchIndexPlans`'s
45
+ * reason: a declaration the author believes is live that silently is not leaves a trashed
46
+ * row in every list with no error anywhere.
47
+ */
48
+ export function entityStatePlans(moduleId, declarations) {
49
+ if (!declarations?.length)
50
+ return [];
51
+ const plans = [];
52
+ const seen = new Set();
53
+ for (const decl of declarations) {
54
+ const where = `${moduleId} entityStates['${decl.entityType}']`;
55
+ if (seen.has(decl.entityType))
56
+ throw new Error(`entity state: ${where} is declared twice`);
57
+ seen.add(decl.entityType);
58
+ if (!decl.table) {
59
+ throw new Error(`entity state: ${where} carries no table — declare \`archive\`/\`trash\` on the entity and ` +
60
+ 'spread `manifestEntities()`, so the entity registry supplies it');
61
+ }
62
+ if (!decl.archivePermission && !decl.trashPermission) {
63
+ throw new Error(`entity state: ${where} declares neither an archive nor a trash permission`);
64
+ }
65
+ plans.push({
66
+ moduleId,
67
+ entityType: decl.entityType,
68
+ table: assertSqlIdentifier('entity state', 'a table', decl.table, where),
69
+ idColumn: assertSqlIdentifier('entity state', 'an id column', decl.idColumn ?? 'id', where),
70
+ ...(decl.archivePermission ? { archivePermission: decl.archivePermission } : {}),
71
+ ...(decl.trashPermission ? { trashPermission: decl.trashPermission } : {}),
72
+ });
73
+ }
74
+ return plans;
75
+ }
76
+ /**
77
+ * The migrations that add the columns — one per column, so declaring `trash` on an entity
78
+ * that already declared `archive` adds one column and re-runs nothing.
79
+ *
80
+ * **The version is the declaration**, as for search and list indexes. Unlike theirs, this DDL
81
+ * is not drop-then-create: a column holds the state, and dropping it would un-trash every row.
82
+ * So it runs once per column, ever. Withdrawing a declaration leaves the column where it is,
83
+ * holding what it held; re-declaring it finds the migration already applied.
84
+ *
85
+ * Applied after the module's own migrations (the table must exist) and before its list
86
+ * indexes (their partial `WHERE` names these columns) — `moduleMigrations` writes that order.
87
+ */
88
+ export function entityStateMigrations(moduleId, declarations) {
89
+ const out = [];
90
+ for (const plan of entityStatePlans(moduleId, declarations)) {
91
+ if (plan.archivePermission) {
92
+ out.push({
93
+ version: `state/${plan.entityType}:archive`,
94
+ sql: `ALTER TABLE ${plan.table} ADD COLUMN ${ARCHIVED_AT_COLUMN} TEXT;`,
95
+ });
96
+ }
97
+ if (plan.trashPermission) {
98
+ out.push({
99
+ version: `state/${plan.entityType}:trash`,
100
+ sql: `ALTER TABLE ${plan.table} ADD COLUMN ${TRASHED_AT_COLUMN} TEXT;`,
101
+ });
102
+ }
103
+ // After the columns they name. Versioned by which columns they guard, so declaring a trash
104
+ // on an archivable entity rebuilds them to guard both.
105
+ out.push({ version: `state/${plan.entityType}:guard:${columnNamesOf(plan).join('+')}`, sql: entityStateTriggerDdl(plan) });
106
+ }
107
+ return out;
108
+ }
109
+ const columnNamesOf = (plan) => [
110
+ ...(plan.archivePermission ? ['archive'] : []),
111
+ ...(plan.trashPermission ? ['trash'] : []),
112
+ ];
113
+ /** The prefix of every trigger this module derives — kernel-owned, so the reserved one. */
114
+ export const ENTITY_STATE_TRIGGER_PREFIX = '_substrat_state_';
115
+ /**
116
+ * The kernel's authorization for ONE move (#119, Codex r2): the row `ctx.archive` & co. write
117
+ * just before their `UPDATE` and delete just after it, in the same transaction. The update
118
+ * trigger below refuses any change to the columns that has no such row — so the only writer the
119
+ * columns admit is the one that can write a `_substrat_*` TABLE, which `ctx.sql` refuses by name
120
+ * (a far simpler reading than finding an assignment target in an expression).
121
+ *
122
+ * Never holds a row between operations: written and removed inside one move, and a move that
123
+ * throws rolls back with its operation. Shared by both adapters' `KERNEL_DDL`, like the other
124
+ * kernel-owned spine tables, so the two cannot part company.
125
+ */
126
+ export const ENTITY_STATE_MOVES_TABLE = '_substrat_state_moves';
127
+ export const ENTITY_STATE_MOVES_DDL = `
128
+ CREATE TABLE IF NOT EXISTS _substrat_state_moves (
129
+ entity_type TEXT NOT NULL,
130
+ entity_id TEXT NOT NULL,
131
+ PRIMARY KEY (entity_type, entity_id)
132
+ );
133
+ `;
134
+ /** A string literal for SQL text: the entity type is a declaration, still quoted rather than trusted. */
135
+ const literal = (value) => `'${value.replace(/'/g, "''")}'`;
136
+ /**
137
+ * The invariant below the column guard, as two triggers on the entity's table.
138
+ *
139
+ * `ctx.sql` refuses the writes that would set the columns (`assertNoReservedColumnWrite`), but
140
+ * that is a reading of SQL text, and a reading can miss a form — it did, twice. These hold
141
+ * whatever the text says:
142
+ *
143
+ * - **born** — `BEFORE INSERT`: a row is never inserted archived or trashed. Every row enters
144
+ * active.
145
+ * - **moved** — `BEFORE UPDATE OF` the columns: a change to either aborts unless the kernel's
146
+ * authorization row for this entity exists (`ENTITY_STATE_MOVES_TABLE`), which only a move
147
+ * writes. An `UPDATE` that does not name the columns does not fire it.
148
+ *
149
+ * Drop-then-create, like the search triggers, and re-run after a dump load: a load drops the
150
+ * table and with it the triggers, then inserts the rows first — a restored binned row is
151
+ * legitimately born trashed, so the triggers are put back only after them.
152
+ */
153
+ export function entityStateTriggerDdl(plan) {
154
+ const born = `${ENTITY_STATE_TRIGGER_PREFIX}${plan.table}_born`;
155
+ const moved = `${ENTITY_STATE_TRIGGER_PREFIX}${plan.table}_moved`;
156
+ const columns = [
157
+ ...(plan.archivePermission ? [ARCHIVED_AT_COLUMN] : []),
158
+ ...(plan.trashPermission ? [TRASHED_AT_COLUMN] : []),
159
+ ];
160
+ return [
161
+ `DROP TRIGGER IF EXISTS ${born};`,
162
+ `CREATE TRIGGER ${born} BEFORE INSERT ON ${plan.table} WHEN ${columns.map((c) => `NEW.${c} IS NOT NULL`).join(' OR ')} BEGIN`,
163
+ ` SELECT RAISE(ABORT, 'a row is never inserted archived or trashed - ctx.archive and ctx.trash move it (#119)');`,
164
+ `END;`,
165
+ `DROP TRIGGER IF EXISTS ${moved};`,
166
+ `CREATE TRIGGER ${moved} BEFORE UPDATE OF ${columns.join(', ')} ON ${plan.table}`,
167
+ `WHEN NOT EXISTS (SELECT 1 FROM ${ENTITY_STATE_MOVES_TABLE} WHERE entity_type = ${literal(plan.entityType)} AND entity_id = OLD.${plan.idColumn}) BEGIN`,
168
+ ` SELECT RAISE(ABORT, 'archive and trash state moves only through ctx.archive, ctx.trash and ctx.restore (#119)');`,
169
+ `END;`,
170
+ ].join('\n');
171
+ }
172
+ /**
173
+ * Add one module's plans to a scope's registry, refusing a second declaration of a type — the
174
+ * table is one module's, and two answers to "who may trash it" is no answer — and a key the
175
+ * module does not declare. The entity is the module's, so its keys are too: a key nobody
176
+ * declared reaches no role, no grant shape and no `PERMISSIONS.md`, and would make the verb
177
+ * look gated while nobody could ever pass it.
178
+ */
179
+ export function addStatePlans(byType, moduleId, declarations, declaredKeys) {
180
+ for (const plan of entityStatePlans(moduleId, declarations)) {
181
+ for (const key of [plan.archivePermission, plan.trashPermission]) {
182
+ if (key && !declaredKeys.some((p) => p.key === key)) {
183
+ throw new Error(`entity state: ${moduleId} gates '${plan.entityType}' on '${key}', which it does not declare in \`permissions\``);
184
+ }
185
+ }
186
+ const existing = byType.get(plan.entityType);
187
+ if (existing) {
188
+ throw new Error(`entity state: '${plan.entityType}' is declared archivable/trashable by both ` +
189
+ `'${existing.moduleId}' and '${plan.moduleId}' — one entity type, one owner`);
190
+ }
191
+ byType.set(plan.entityType, plan);
192
+ }
193
+ }
194
+ /** The tables whose rows carry the columns, lowercased as SQLite resolves a name — `guardSpine`'s input. */
195
+ export const statefulTablesOf = (plans) => new Set([...plans.values()].map((p) => p.table.toLowerCase()));
196
+ export const stateColumnsOf = (plan) => ({
197
+ archive: plan.archivePermission !== undefined,
198
+ trash: plan.trashPermission !== undefined,
199
+ });
200
+ /** The views an entity with these columns has — `active` always, then one per column. */
201
+ export function viewsOf(columns) {
202
+ return ['active', ...(columns.archive ? ['archived'] : []), ...(columns.trash ? ['trashed'] : [])];
203
+ }
204
+ export function entityStateWhere(entityType, columns, view, alias) {
205
+ const asked = view ?? 'active';
206
+ if ((asked === 'archived' && !columns?.archive) || (asked === 'trashed' && !columns?.trash)) {
207
+ throw substratError('validation_failed', `'${entityType}' has no ${asked} view — it declares no ${asked === 'archived' ? 'archive' : 'trash'}`, { errors: [{ path: 'view', message: `not declared for '${entityType}'` }] });
208
+ }
209
+ if (!columns)
210
+ return undefined;
211
+ const col = (name) => (alias ? `${alias}.${name}` : name);
212
+ if (asked === 'trashed')
213
+ return `${col(TRASHED_AT_COLUMN)} IS NOT NULL`;
214
+ const terms = [];
215
+ if (columns.archive)
216
+ terms.push(`${col(ARCHIVED_AT_COLUMN)} IS ${asked === 'archived' ? 'NOT ' : ''}NULL`);
217
+ if (columns.trash)
218
+ terms.push(`${col(TRASHED_AT_COLUMN)} IS NULL`);
219
+ return terms.join(' AND ');
220
+ }
221
+ const stateOf = (row) => row.trashed_at !== null ? 'trashed' : row.archived_at !== null ? 'archived' : 'active';
222
+ /** One row's two columns, or `undefined` when the row does not exist. */
223
+ function readStateRow(sql, plan, entityId) {
224
+ const columns = stateColumnsOf(plan);
225
+ return sql.query(`SELECT ${columns.archive ? ARCHIVED_AT_COLUMN : 'NULL'} AS archived_at, ` +
226
+ `${columns.trash ? TRASHED_AT_COLUMN : 'NULL'} AS trashed_at ` +
227
+ `FROM ${plan.table} WHERE ${plan.idColumn} = ?`, [entityId])[0];
228
+ }
229
+ /**
230
+ * The plan for `entityType`, and the key one of its states is gated on — or `validation_failed`
231
+ * naming what the entity does not declare. Shared by the verbs and the trashed readers, so
232
+ * "declares no trash" is one sentence.
233
+ */
234
+ export function stateKeyOf(plans, verb, entityType, which) {
235
+ const plan = plans.get(entityType);
236
+ const key = which === 'archive' ? plan?.archivePermission : plan?.trashPermission;
237
+ if (!plan || !key) {
238
+ throw substratError('validation_failed', `${verb}: '${entityType}' declares no ${which}`, {
239
+ errors: [{ path: 'entityType', message: `'${entityType}' declares no ${which}` }],
240
+ });
241
+ }
242
+ return { plan, key };
243
+ }
244
+ const COLUMN = { archived_at: ARCHIVED_AT_COLUMN, trashed_at: TRASHED_AT_COLUMN };
245
+ const MOVES = {
246
+ archive: { verb: 'ctx.archive', which: 'archive', from: ['active'], column: 'archived_at', set: true, type: ENTITY_ARCHIVED },
247
+ unarchive: { verb: 'ctx.unarchive', which: 'archive', from: ['archived'], column: 'archived_at', set: false, type: ENTITY_UNARCHIVED },
248
+ // Archived rows may be trashed: "delete this old thing" is the commonest reason to open an
249
+ // archive at all. The archive survives the trip, which is what `restore` relies on.
250
+ trash: { verb: 'ctx.trash', which: 'trash', from: ['active', 'archived'], column: 'trashed_at', set: true, type: ENTITY_TRASHED },
251
+ restore: { verb: 'ctx.restore', which: 'trash', from: ['trashed'], column: 'trashed_at', set: false, type: ENTITY_RESTORED },
252
+ };
253
+ export function createEntityStateVerbs(deps) {
254
+ const move = async (m, entity) => {
255
+ deps.assertWrites(m.verb);
256
+ const { plan, key } = stateKeyOf(deps.plans, m.verb, entity.entityType, m.which);
257
+ // The DECLARED key, on THIS entity, before anything about the row is read: whether it
258
+ // exists is not something a caller without the key gets to learn.
259
+ assertAllowed(await deps.check(key, entity));
260
+ const row = readStateRow(deps.sql, plan, entity.entityId);
261
+ if (!row) {
262
+ throw substratError('not_found', `${m.verb}: ${entity.entityType}:${entity.entityId} does not exist`);
263
+ }
264
+ const from = stateOf(row);
265
+ if (!m.from.includes(from)) {
266
+ throw substratError('conflict', `${m.verb}: ${entity.entityType}:${entity.entityId} is ${from} — ` +
267
+ `only ${m.from.join(' or ')} can be moved this way`, { reason: 'invalid_transition' });
268
+ }
269
+ const at = m.set ? deps.now : null;
270
+ // The authorization the update trigger asks for, held for exactly this one statement.
271
+ const authorize = [entity.entityType, entity.entityId];
272
+ deps.sql.exec(`INSERT OR IGNORE INTO ${ENTITY_STATE_MOVES_TABLE} (entity_type, entity_id) VALUES (?, ?)`, authorize);
273
+ try {
274
+ const { changes } = deps.sql.exec(`UPDATE ${plan.table} SET ${COLUMN[m.column]} = ? WHERE ${plan.idColumn} = ?`, [
275
+ at,
276
+ entity.entityId,
277
+ ]);
278
+ // The row exists (read above), so an UPDATE that changed nothing was swallowed — a trigger's
279
+ // RAISE(IGNORE), say. Recording a move that did not happen is the one thing this must not do.
280
+ // `0`, not `!== 1`: a Durable Object's count includes the index rows the move rewrote.
281
+ if (changes === 0) {
282
+ throw substratError('internal', `${m.verb}: the update of ${entity.entityType}:${entity.entityId} changed no row — nothing was recorded`);
283
+ }
284
+ }
285
+ finally {
286
+ deps.sql.exec(`DELETE FROM ${ENTITY_STATE_MOVES_TABLE} WHERE entity_type = ? AND entity_id = ?`, authorize);
287
+ }
288
+ const to = stateOf({ ...row, [m.column]: at });
289
+ const payload = entityStateChangedPayload.parse({ entity, from, to }); // strips extra keys
290
+ deps.emit({ type: m.type, schemaVersion: 1, entity: payload.entity, piiClass: 'none', payload });
291
+ return to;
292
+ };
293
+ return {
294
+ archive: (entity) => move(MOVES.archive, entity),
295
+ unarchive: (entity) => move(MOVES.unarchive, entity),
296
+ trash: (entity) => move(MOVES.trash, entity),
297
+ restore: (entity) => move(MOVES.restore, entity),
298
+ entityState: (entity) => {
299
+ const plan = deps.plans.get(entity.entityType);
300
+ if (!plan) {
301
+ throw substratError('validation_failed', `ctx.entityState: '${entity.entityType}' declares no archive or trash — declare one on the entity`);
302
+ }
303
+ const row = readStateRow(deps.sql, plan, entity.entityId);
304
+ return row ? stateOf(row) : null;
305
+ },
306
+ };
307
+ }
308
+ /**
309
+ * After runtime DDL (#119, Codex r3): does every stateful table still carry what the kernel
310
+ * derived for it? The guard refuses the DDL known to take it away; this is the check that does not
311
+ * depend on having known. Read from `main`'s own catalogue, so a same-named temp object cannot
312
+ * stand in for the real table. Throws — the operation and its DDL roll back — rather than
313
+ * re-deriving: a schema the kernel did not expect is not one to repair silently.
314
+ *
315
+ * `indexes` names the derived list indexes each stateful table must keep, by table.
316
+ */
317
+ export function assertEntityStateIntact(sql, plans, indexes = new Map()) {
318
+ for (const plan of plans.values()) {
319
+ const fail = (what) => {
320
+ throw substratError('internal', `runtime DDL left '${plan.table}' without ${what} — its archive/trash guarantees depend on it; nothing was changed`);
321
+ };
322
+ const objects = new Map(sql
323
+ .query(`SELECT type, name FROM main.sqlite_master WHERE tbl_name = ? COLLATE NOCASE`, [plan.table])
324
+ .map((r) => [r.name.toLowerCase(), r.type]));
325
+ if (objects.get(plan.table.toLowerCase()) !== 'table')
326
+ fail('its table');
327
+ const columns = new Set(sql
328
+ .query(`SELECT name FROM pragma_table_info(?, 'main')`, [plan.table])
329
+ .map((r) => r.name.toLowerCase()));
330
+ if (plan.archivePermission && !columns.has(ARCHIVED_AT_COLUMN))
331
+ fail(ARCHIVED_AT_COLUMN);
332
+ if (plan.trashPermission && !columns.has(TRASHED_AT_COLUMN))
333
+ fail(TRASHED_AT_COLUMN);
334
+ for (const suffix of ['born', 'moved']) {
335
+ const trigger = `${ENTITY_STATE_TRIGGER_PREFIX}${plan.table}_${suffix}`.toLowerCase();
336
+ if (objects.get(trigger) !== 'trigger')
337
+ fail(`its ${suffix} trigger`);
338
+ }
339
+ for (const index of indexes.get(plan.table) ?? []) {
340
+ if (objects.get(index.toLowerCase()) !== 'index')
341
+ fail(`its list index ${index}`);
342
+ }
343
+ }
344
+ }
345
+ //# sourceMappingURL=entity-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"entity-state.js","sourceRoot":"","sources":["../src/entity-state.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,143 @@
1
+ import { type ErrorCode, type FindingEntry, type FindingEvidence, type FindingFilter, type FindingKind, type FindingRuleEntry, type FindingRuleInput, type FindingSeverity, type FindingStatusInput, type TenantId } from '@substrat-run/contracts';
2
+ import type { RedactionSql } from './subject-redaction.js';
3
+ /**
4
+ * How long a finding outlives its last occurrence — the issue store's window, since a Recurring
5
+ * finding is the tenant's projection of an issue and should be recognisable for as long.
6
+ */
7
+ export declare const FINDING_RETENTION_DAYS = 180;
8
+ /**
9
+ * The findings store (#1748), as statements both adapters run against their directory — the
10
+ * SQLite host over better-sqlite3, the hosted one inside ControlPlaneDO — so the lifecycle is
11
+ * written once. `sql` is the row-returning executor subject erasure already uses.
12
+ *
13
+ * Detection is ON WRITE: the call that records an ops failure or a sweep run also observes the
14
+ * finding it implies, so there is no cron and no scan. Per occurrence that is one indexed rule
15
+ * lookup and one primary-key upsert, over evidence that is already bounded upstream.
16
+ */
17
+ export declare const FINDINGS_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_findings (\n id TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL,\n kind TEXT NOT NULL,\n subject TEXT NOT NULL,\n status TEXT NOT NULL,\n regressed INTEGER NOT NULL DEFAULT 0,\n severity TEXT NOT NULL,\n title TEXT NOT NULL,\n operation TEXT,\n codes TEXT NOT NULL DEFAULT '[]',\n vertical TEXT,\n scope_id TEXT,\n seen_count INTEGER NOT NULL,\n first_seen TEXT NOT NULL,\n last_seen TEXT NOT NULL,\n last_version TEXT,\n resolved_version TEXT,\n resolved_at TEXT,\n resolution TEXT,\n acknowledged_at TEXT,\n rule_id TEXT,\n evidence TEXT NOT NULL,\n likely_cause TEXT,\n UNIQUE (tenant_id, kind, subject)\n );\n CREATE INDEX IF NOT EXISTS _substrat_findings_tenant_seen ON _substrat_findings (tenant_id, last_seen);\n CREATE INDEX IF NOT EXISTS _substrat_findings_seen ON _substrat_findings (last_seen);\n CREATE INDEX IF NOT EXISTS _substrat_findings_quiet ON _substrat_findings (last_seen) WHERE status IN ('open', 'acked');\n CREATE TABLE IF NOT EXISTS _substrat_finding_rules (\n id TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL,\n kind TEXT,\n operation TEXT,\n code TEXT,\n subject TEXT,\n expires_at TEXT NOT NULL,\n reason TEXT NOT NULL,\n created_by TEXT NOT NULL,\n created_at TEXT NOT NULL\n );\n CREATE INDEX IF NOT EXISTS _substrat_finding_rules_tenant ON _substrat_finding_rules (tenant_id, expires_at);\n CREATE INDEX IF NOT EXISTS _substrat_finding_rules_expiry ON _substrat_finding_rules (expires_at);\n";
18
+ /** One occurrence, as a source hands it to `observeFinding`. */
19
+ export interface FindingObservation {
20
+ tenantId: string;
21
+ kind: FindingKind;
22
+ subject: string;
23
+ severity: FindingSeverity;
24
+ title: string;
25
+ operation: string | null;
26
+ code: ErrorCode | null;
27
+ vertical: string | null;
28
+ scopeId: string | null;
29
+ version: string | null;
30
+ evidence: FindingEvidence;
31
+ }
32
+ /**
33
+ * The Recurring source: a tenant's own ops failure, keyed on the same fingerprint as the fleet
34
+ * issue. A failure with no tenant is the platform's own and stays in the staff issue store.
35
+ */
36
+ export declare function findingOfOpsFailure(f: {
37
+ tenantId?: string | null;
38
+ scopeId?: string | null;
39
+ operation: string;
40
+ stage?: string | null;
41
+ code?: ErrorCode | null;
42
+ vertical?: string | null;
43
+ version?: string | null;
44
+ }): FindingObservation | null;
45
+ /**
46
+ * The Invariant and Drift sources: a scheduled run that failed (expected 0 failed runs,
47
+ * observed N) and a freshness expectation judged stale. Every other sweep row implies nothing.
48
+ */
49
+ export declare function findingOfSweepRun(r: {
50
+ kind: string;
51
+ unit: string;
52
+ outcome: string;
53
+ tenantId?: string | null;
54
+ scopeId?: string | null;
55
+ vertical?: string | null;
56
+ version?: string | null;
57
+ operation?: string | null;
58
+ eventType?: string | null;
59
+ }): FindingObservation | null;
60
+ /**
61
+ * Record one occurrence on its finding, opening it if new. The transitions, in order:
62
+ * - an unexpired rule covers it → `suppressed` (counted all the same: suppressing hides the
63
+ * finding, never the evidence);
64
+ * - it was `resolved` → `open`, `regressed`, and its likely cause is worked out afresh from
65
+ * THIS regression: the version it came back under, when that is not the one it was resolved
66
+ * under, and none otherwise — never a cause left over from an earlier cycle;
67
+ * - it was `suppressed` and no rule covers it any longer → `open`;
68
+ * - otherwise the status stands (`acked` stays acked: somebody is already on it).
69
+ */
70
+ export declare function observeFinding(sql: RedactionSql, o: FindingObservation, at: string): void;
71
+ /**
72
+ * Findings, most recently seen first. No cursor, like the issue read it projects: the
73
+ * cardinality is the number of distinct shapes a tenant has, and `limit` bounds it.
74
+ */
75
+ export declare function listFindings(sql: RedactionSql, filter?: FindingFilter): FindingEntry[];
76
+ export interface FindingChange {
77
+ before: FindingEntry;
78
+ after: FindingEntry;
79
+ }
80
+ /**
81
+ * The audit row a findings mutation writes — `recordAdmin`'s action, target, before and after.
82
+ * Every mutation below hands it to `audit` INSIDE its own statements, so the adapter that runs
83
+ * them in one unit commits the change and its row together, or neither (#1748).
84
+ */
85
+ export interface FindingAudit {
86
+ action: 'setFindingStatus' | 'createFindingRule' | 'revokeFindingRule' | 'resolveStaleFinding';
87
+ target: {
88
+ tenantId: TenantId;
89
+ vertical: string | null;
90
+ };
91
+ before: unknown;
92
+ after: unknown;
93
+ }
94
+ /**
95
+ * A person's verdict: acknowledge, resolve, or reopen. Keyed on (tenant, id), so a tenant can
96
+ * never move another tenant's finding by guessing its id. Reopening a suppressed finding lifts
97
+ * it out of its rule for now; the next occurrence the rule still covers suppresses it again —
98
+ * revoking the rule is what ends a suppression.
99
+ */
100
+ export declare function setFindingStatus(sql: RedactionSql, tenantId: TenantId, id: string, status: FindingStatusInput, at: string, audit: (row: FindingAudit) => void): FindingChange | undefined;
101
+ /**
102
+ * Create a suppress rule and apply it to the findings it covers now: each one not already
103
+ * resolved becomes `suppressed` under it. Returns the rule and the ids it suppressed, for the
104
+ * caller's audit row. The input is parsed by the caller (`findingRuleInput`); the expiry is
105
+ * held here (`assertFindingRuleExpiry`), against the same `at` every other write in the call uses.
106
+ */
107
+ export declare function createFindingRule(sql: RedactionSql, tenantId: TenantId, input: FindingRuleInput, createdBy: string, at: string, audit: (row: FindingAudit) => void): {
108
+ rule: FindingRuleEntry;
109
+ suppressed: string[];
110
+ };
111
+ /**
112
+ * End a rule now. Its findings stay suppressed until their next occurrence, which then opens
113
+ * them — the same thing an expiry does, brought forward.
114
+ */
115
+ export declare function revokeFindingRule(sql: RedactionSql, tenantId: TenantId, ruleId: string, at: string, audit: (row: FindingAudit) => void): {
116
+ before: FindingRuleEntry;
117
+ after: FindingRuleEntry;
118
+ } | undefined;
119
+ /** A tenant's rules, newest first; `activeAt` keeps only those unexpired at that instant. */
120
+ export declare function listFindingRules(sql: RedactionSql, tenantId: TenantId | undefined, activeAt?: string, limit?: number): FindingRuleEntry[];
121
+ /** What one `pruneFindings` pass did. */
122
+ export interface FindingPruneReport {
123
+ /** Resolved and suppressed findings deleted. */
124
+ deleted: number;
125
+ /** Open or acked findings resolved as `stale`, each audited. */
126
+ staled: number;
127
+ /** Expired suppress rules deleted. */
128
+ rulesDeleted: number;
129
+ }
130
+ /**
131
+ * The findings retention pass, bounded by `limit` per step, oldest first. Run in ONE unit with
132
+ * `audit`, which the adapter points at its own admin-log write, so a stale resolution never
133
+ * lands without its row.
134
+ *
135
+ * An `open` or `acked` finding is never deleted for going quiet: deleting it would take it out
136
+ * of the inbox of whoever was watching it with nothing said. One quiet for the whole
137
+ * `FINDING_RETENTION_DAYS` is RESOLVED as `stale` instead, and audited. A `resolved` or
138
+ * `suppressed` finding is deleted once both its last occurrence and its last resolve are past
139
+ * the horizon — so a stale-resolved finding stays readable for one more window. An expired rule
140
+ * is deleted once its expiry is past the horizon; the admin log keeps its story.
141
+ */
142
+ export declare function pruneFindings(sql: RedactionSql, nowMs: number, limit: number, audit: (row: FindingAudit) => void): FindingPruneReport;
143
+ //# sourceMappingURL=findings.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findings.d.ts","sourceRoot":"","sources":["../src/findings.ts"],"names":[],"mappings":"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"}