@catalyst-cloud/schema 0.1.14 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.ts +109 -5
- package/src/migrations.generated.ts +27 -0
- package/src/mirror.ts +231 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catalyst-cloud/schema",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed Drizzle schema = single source of truth for the per-tenant Mirror DO SQLite store (CTC-13 / ADR-0002). Shared by the mirror Worker, the host-sync replica, and the browser OPFS replica.",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
projects,
|
|
20
20
|
cycles,
|
|
21
21
|
workflow_states,
|
|
22
|
+
// CTC-624: the tenant's per-team workflow DECISION, projected from the D1 registry (ADR-0033).
|
|
23
|
+
team_workflow_mapping,
|
|
22
24
|
initiatives,
|
|
23
25
|
project_initiatives,
|
|
24
26
|
comments,
|
|
@@ -42,6 +44,10 @@ import {
|
|
|
42
44
|
pr_events,
|
|
43
45
|
pr_ancillary_backfill,
|
|
44
46
|
push_subscriptions,
|
|
47
|
+
pushes,
|
|
48
|
+
deployments,
|
|
49
|
+
deployment_statuses,
|
|
50
|
+
pr_review_threads,
|
|
45
51
|
} from "./mirror.js";
|
|
46
52
|
|
|
47
53
|
export * from "./mirror.js";
|
|
@@ -79,6 +85,8 @@ export const mirrorSchema = {
|
|
|
79
85
|
projects,
|
|
80
86
|
cycles,
|
|
81
87
|
workflow_states,
|
|
88
|
+
// CTC-624: projected from the D1 registry, the one mirror table whose authority is not a provider.
|
|
89
|
+
team_workflow_mapping,
|
|
82
90
|
initiatives,
|
|
83
91
|
project_initiatives,
|
|
84
92
|
comments,
|
|
@@ -113,13 +121,27 @@ export const mirrorSchema = {
|
|
|
113
121
|
pr_commits,
|
|
114
122
|
pr_events,
|
|
115
123
|
pr_ancillary_backfill,
|
|
116
|
-
// CTC-619: a member's Web Push subscription.
|
|
117
|
-
//
|
|
124
|
+
// CTC-619: a member's Web Push subscription. ⚠️ Whether it rides the change feed is DISPUTED —
|
|
125
|
+
// see mirrorEntityTables below and CTC-671.
|
|
118
126
|
push_subscriptions,
|
|
127
|
+
// CTC-667: the current head of a pushed git ref — the orchestrator's rebase-detection signal.
|
|
128
|
+
pushes,
|
|
129
|
+
// CTC-667: a GitHub deployment, its status transitions, and a PR review thread's resolution state
|
|
130
|
+
// — the remaining payload types host dispatch consumes (CTL-1929's last smee tunnel).
|
|
131
|
+
deployments,
|
|
132
|
+
deployment_statuses,
|
|
133
|
+
pr_review_threads,
|
|
119
134
|
} as const;
|
|
120
135
|
|
|
121
136
|
/**
|
|
122
|
-
*
|
|
137
|
+
* ⭐ CTC-671 — THE SINGLE DEFINITION OF CHANGE-FEED MEMBERSHIP. The keys are the wire `entity`
|
|
138
|
+
* names, and everything that answers "is this row safe for every reader of the account's feed"
|
|
139
|
+
* derives from this object: `FEED_ENTITY_NAMES` below, `@catalyst-cloud/types`' `EntityName`,
|
|
140
|
+
* `SNAPSHOT_TABLES`, and the SDK's published `ENTITY_NAMES`. A table that is mirrored but must NEVER
|
|
141
|
+
* be broadcast account-wide goes in `nonFeedEntityTables` instead — see its doc for the reason that
|
|
142
|
+
* distinction exists at all.
|
|
143
|
+
*
|
|
144
|
+
* The
|
|
123
145
|
* infra tables — processed_events / change_log / event_log / sync_cursors — are deliberately excluded:
|
|
124
146
|
* they never ride a delta.
|
|
125
147
|
*
|
|
@@ -133,7 +155,7 @@ export const mirrorSchema = {
|
|
|
133
155
|
* into a 200k/7-day ring — the ring would hold under a day and every replica cursor would fall off
|
|
134
156
|
* the end. The arithmetic and the rest of the posture live on the table definition in ./mirror.ts.
|
|
135
157
|
*/
|
|
136
|
-
export const
|
|
158
|
+
export const feedEntityTables = {
|
|
137
159
|
issues,
|
|
138
160
|
labels,
|
|
139
161
|
users,
|
|
@@ -143,6 +165,9 @@ export const mirrorEntityTables = {
|
|
|
143
165
|
projects,
|
|
144
166
|
cycles,
|
|
145
167
|
workflow_states,
|
|
168
|
+
// CTC-624: the tenant's per-team workflow decision. On this plane deliberately — it is how
|
|
169
|
+
// anything reaches a host at all, and the browser board is a legitimate consumer (ADR-0033).
|
|
170
|
+
team_workflow_mapping,
|
|
146
171
|
initiatives,
|
|
147
172
|
project_initiatives,
|
|
148
173
|
comments,
|
|
@@ -161,10 +186,89 @@ export const mirrorEntityTables = {
|
|
|
161
186
|
pr_conversation_comments,
|
|
162
187
|
pr_commits,
|
|
163
188
|
pr_events,
|
|
164
|
-
// CTC-
|
|
189
|
+
// CTC-667: the current head of a pushed git ref (one row per repo+ref) — the orchestrator's
|
|
190
|
+
// rebase-detection signal, and the first of the four payload types CTL's dispatch consumes that
|
|
191
|
+
// the mirror did not carry (CTL-1929's last smee tunnel).
|
|
192
|
+
pushes,
|
|
193
|
+
// CTC-667: a GitHub deployment and its status history — what `phase-monitor-deploy` and the deploy
|
|
194
|
+
// state machine key on (`environment`, `state`, `target_url`, `environment_url`).
|
|
195
|
+
deployments,
|
|
196
|
+
deployment_statuses,
|
|
197
|
+
// CTC-667: a PR review thread's RESOLUTION state — the merge gate AGENTS.md names, and the one
|
|
198
|
+
// fact `pr_review_comments` cannot express (it has no resolution column).
|
|
199
|
+
pr_review_threads,
|
|
200
|
+
} as const;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* CTC-671 — mirrored tables that must NEVER ride the ACCOUNT-WIDE change feed.
|
|
204
|
+
*
|
|
205
|
+
* ⛔ THIS OBJECT IS THE EXCLUSION, AND IT IS STRUCTURAL RATHER THAN SUBTRACTIVE ON PURPOSE. A table
|
|
206
|
+
* listed here is a real mirror table — the DO creates it, migrations produce it, `MIRROR_TABLE_META`
|
|
207
|
+
* carries it (so `@catalyst-cloud/replicate`'s `knownColumns` forward-compat path is unchanged) — but
|
|
208
|
+
* it is not a member of `feedEntityTables`, so no list derived from that object can contain it. There
|
|
209
|
+
* is no filter to forget and no exclusion array to quietly drop: to put a table on the feed you must
|
|
210
|
+
* type its name into `feedEntityTables`, which is a deliberate act.
|
|
211
|
+
*
|
|
212
|
+
* ⭐ WHY, in one case's words. `push_subscriptions` rows are `endpoint` + `p256dh` + `auth` — together
|
|
213
|
+
* the complete capability to push arbitrary notifications to that person's browser. CTC-641 found
|
|
214
|
+
* them being appended to `change_log` and broadcast account-wide to every host replica and every
|
|
215
|
+
* OTHER member's browser OPFS DB. It fixed the leak by narrowing `MirrorDO.appendAndBroadcast` to
|
|
216
|
+
* `@catalyst-cloud/types`' `EntityName` — but left this table inside `mirrorEntityTables`, next to a
|
|
217
|
+
* comment asserting it "rides the change feed like any other entity". The repo then stated two
|
|
218
|
+
* incompatible things about one table and the MACHINE-READABLE one was the wrong one, so the SDK had
|
|
219
|
+
* to carry a hand-written exclusion list to avoid republishing it (CTC-643 / sdk#24). CTC-671 is that
|
|
220
|
+
* second list being deleted: feed membership now has exactly one definition, `feedEntityTables`.
|
|
221
|
+
*
|
|
222
|
+
* ⚠️ A table belongs here, not merely out of `mirrorEntityTables`, when it must still REPLICATE to
|
|
223
|
+
* the surfaces that legitimately hold it while never being broadcast account-wide. Bookkeeping that
|
|
224
|
+
* no consumer should ever see (`pull_sweeps`, `pr_review_backfill`, `fleet_activity_pruned`) stays
|
|
225
|
+
* out of BOTH objects — see their own table docs.
|
|
226
|
+
*/
|
|
227
|
+
export const nonFeedEntityTables = {
|
|
165
228
|
push_subscriptions,
|
|
166
229
|
} as const;
|
|
167
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Every mirrored ENTITY table — the feed-safe ones plus the deliberately-excluded ones.
|
|
233
|
+
*
|
|
234
|
+
* ⚠️ THIS IS NOT THE FEED LIST. It exists so `MIRROR_TABLE_META` keeps describing every replicable
|
|
235
|
+
* table (PKs, soft-delete posture, and the `columns` set that `@catalyst-cloud/replicate` uses as its
|
|
236
|
+
* forward-compat `knownColumns` default). Deriving "safe for every reader of the account feed" from
|
|
237
|
+
* THIS object is the CTC-671 defect; derive it from `feedEntityTables` / `FEED_ENTITY_NAMES`.
|
|
238
|
+
*/
|
|
239
|
+
export const mirrorEntityTables = {
|
|
240
|
+
...feedEntityTables,
|
|
241
|
+
...nonFeedEntityTables,
|
|
242
|
+
} as const;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* ⭐ CTC-671 — THE ONE LIST THAT GOVERNS FEED MEMBERSHIP, as a runtime array.
|
|
246
|
+
*
|
|
247
|
+
* `@catalyst-cloud/types`' `EntityName` is a type alias of `FeedEntityName`, `SNAPSHOT_TABLES` is
|
|
248
|
+
* typed `EntityName`, and `@catalyst-cloud/sdk`'s published `ENTITY_NAMES` is pinned to this array by
|
|
249
|
+
* `test/entity-names-drift.test.ts`. Add a table to `feedEntityTables` and all three follow; leave it
|
|
250
|
+
* out and none of them can name it.
|
|
251
|
+
*/
|
|
252
|
+
export const FEED_ENTITY_NAMES: readonly FeedEntityName[] = keysOf(feedEntityTables);
|
|
253
|
+
|
|
254
|
+
/** The excluded set as a runtime array — so a test can assert the exclusion still DOES something. */
|
|
255
|
+
export const NON_FEED_ENTITY_NAMES: readonly NonFeedEntityName[] = keysOf(nonFeedEntityTables);
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* `Object.keys` with the key type preserved. TypeScript types `Object.keys` as `string[]` because an
|
|
259
|
+
* object may carry extra keys at runtime; these two objects are `as const` literals declared in this
|
|
260
|
+
* file, so the narrowing is sound. Contained here so it is the only such assertion in the module.
|
|
261
|
+
*/
|
|
262
|
+
function keysOf<T extends object>(o: T): (keyof T & string)[] {
|
|
263
|
+
return Object.keys(o) as (keyof T & string)[];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** The wire `entity` name of a FEED-CARRIED table (keys of feedEntityTables) — the contract. */
|
|
267
|
+
export type FeedEntityName = keyof typeof feedEntityTables;
|
|
268
|
+
|
|
269
|
+
/** The name of a mirrored-but-never-broadcast table (keys of nonFeedEntityTables). */
|
|
270
|
+
export type NonFeedEntityName = keyof typeof nonFeedEntityTables;
|
|
271
|
+
|
|
168
272
|
/** The wire `entity` name of a change-feed-carried table (keys of mirrorEntityTables). */
|
|
169
273
|
export type MirrorEntityName = keyof typeof mirrorEntityTables;
|
|
170
274
|
|
|
@@ -179,6 +179,27 @@ export const MIRROR_MIGRATIONS = {
|
|
|
179
179
|
tag: "0023_ambiguous_micromax",
|
|
180
180
|
breakpoints: true,
|
|
181
181
|
},
|
|
182
|
+
{
|
|
183
|
+
idx: 24,
|
|
184
|
+
version: "6",
|
|
185
|
+
when: 1786984427741,
|
|
186
|
+
tag: "0024_long_spencer_smythe",
|
|
187
|
+
breakpoints: true,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
idx: 25,
|
|
191
|
+
version: "6",
|
|
192
|
+
when: 1787013352521,
|
|
193
|
+
tag: "0025_outgoing_morgan_stark",
|
|
194
|
+
breakpoints: true,
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
idx: 26,
|
|
198
|
+
version: "6",
|
|
199
|
+
when: 1787014505223,
|
|
200
|
+
tag: "0026_curvy_micromacro",
|
|
201
|
+
breakpoints: true,
|
|
202
|
+
},
|
|
182
203
|
],
|
|
183
204
|
},
|
|
184
205
|
migrations: {
|
|
@@ -228,5 +249,11 @@ export const MIRROR_MIGRATIONS = {
|
|
|
228
249
|
"CREATE TABLE `workflow_states` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`team_id` text,\n\t`name` text,\n\t`type` text,\n\t`position` real,\n\t`color` text,\n\t`archived_at` integer,\n\t`updated_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_workflow_states_team` ON `workflow_states` (`team_id`);--> statement-breakpoint\nALTER TABLE `issues` ADD `state_type` text;--> statement-breakpoint\nALTER TABLE `issues` ADD `state_position` real;",
|
|
229
250
|
"0023_ambiguous_micromax":
|
|
230
251
|
"CREATE TABLE `push_subscriptions` (\n\t`endpoint` text PRIMARY KEY NOT NULL,\n\t`member_id` text NOT NULL,\n\t`p256dh` text NOT NULL,\n\t`auth` text NOT NULL,\n\t`created_at` integer,\n\t`last_used_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_push_subscriptions_member` ON `push_subscriptions` (`member_id`);",
|
|
252
|
+
"0024_long_spencer_smythe":
|
|
253
|
+
"CREATE TABLE `team_workflow_mapping` (\n\t`team_id` text NOT NULL,\n\t`slot` text NOT NULL,\n\t`linear_state_id` text NOT NULL,\n\t`linear_state_name` text,\n\t`linear_state_type` text,\n\t`workflow_rev` integer,\n\t`updated_at` integer,\n\t`removed_at` integer,\n\tPRIMARY KEY(`team_id`, `slot`)\n);\n",
|
|
254
|
+
"0025_outgoing_morgan_stark":
|
|
255
|
+
"CREATE TABLE `pushes` (\n\t`repo_id` text NOT NULL,\n\t`ref` text NOT NULL,\n\t`before` text,\n\t`after` text,\n\t`forced` integer,\n\t`created` integer,\n\t`deleted` integer,\n\t`base_ref` text,\n\t`pusher_id` text,\n\t`head_commit_sha` text,\n\t`updated_at` integer,\n\tPRIMARY KEY(`repo_id`, `ref`)\n);\n",
|
|
256
|
+
"0026_curvy_micromacro":
|
|
257
|
+
"CREATE TABLE `deployment_statuses` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`deployment_id` text NOT NULL,\n\t`state` text,\n\t`environment` text,\n\t`target_url` text,\n\t`environment_url` text,\n\t`description` text,\n\t`creator_id` text,\n\t`created_at` integer,\n\t`updated_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_deployment_statuses_deployment` ON `deployment_statuses` (`deployment_id`,`created_at`);--> statement-breakpoint\nCREATE TABLE `deployments` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`ref` text,\n\t`sha` text,\n\t`task` text,\n\t`environment` text,\n\t`production_environment` integer,\n\t`transient_environment` integer,\n\t`description` text,\n\t`creator_id` text,\n\t`created_at` integer,\n\t`updated_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_deployments_repo_env` ON `deployments` (`repo_id`,`environment`,`created_at`);--> statement-breakpoint\nCREATE TABLE `pr_review_threads` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`resolved` integer,\n\t`resolved_at` integer,\n\t`resolver_id` text,\n\t`first_comment_id` text,\n\t`comment_count` integer,\n\t`updated_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_pr_review_threads_pr` ON `pr_review_threads` (`repo_id`,`pr_number`);",
|
|
231
258
|
},
|
|
232
259
|
} as const;
|
package/src/mirror.ts
CHANGED
|
@@ -318,6 +318,51 @@ export const cycles = sqliteTable("cycles", {
|
|
|
318
318
|
// (the prerequisite CTC-277's kanban board blocks on). Small, workspace-wide set — full sweep each
|
|
319
319
|
// pass, same discipline as `labels`/`cycles`. No `removed_at`: Linear ARCHIVES a workflow state rather
|
|
320
320
|
// than deleting it (`archived_at`), same convention `issues.archived_at` already uses.
|
|
321
|
+
/**
|
|
322
|
+
* CTC-624 — the tenant's per-team workflow DECISION, replicated to hosts and the browser.
|
|
323
|
+
*
|
|
324
|
+
* ⭐ THIS IS THE ONE MIRROR TABLE WHOSE AUTHORITY IS NOT A PROVIDER. Every other table here is a
|
|
325
|
+
* projection of something Linear or GitHub owns; this is a projection of the **D1 registry**
|
|
326
|
+
* (`team_workflow_mapping`), which is where the tenant's choice actually lives. It rides this plane
|
|
327
|
+
* because the plane is how anything reaches a host at all, and ADR-0033 records why that does not
|
|
328
|
+
* re-open ADR-0028: the states themselves stay a mirrored Linear fact (`workflow_states`, below),
|
|
329
|
+
* while "which state means dispatchable" is a decision Linear has no field for.
|
|
330
|
+
*
|
|
331
|
+
* ⛔ `linear_state_name` / `linear_state_type` ARE DISPLAY SNAPSHOTS AND MUST NEVER BE JOINED ON.
|
|
332
|
+
* CTC-403: a Linear-to-Linear import preserved every human identifier and re-minted ~248 state UUIDs,
|
|
333
|
+
* so every cache still MATCHED by name while pointing at nothing. `linear_state_id` is the authority;
|
|
334
|
+
* a consumer resolving a slot joins THAT against `workflow_states` and finds the row absent when the
|
|
335
|
+
* workspace has been re-minted. That absence is the alarm, and it only works because the join key is
|
|
336
|
+
* the id.
|
|
337
|
+
*
|
|
338
|
+
* ⭐ `workflow_rev` RIDES EVERY ROW, and that is what makes the receipt possible. A host reads the max
|
|
339
|
+
* `workflow_rev` present in its own replica and echoes it on `/connect` (CTC-628) — read at RUNTIME
|
|
340
|
+
* FROM THE REPLICA, never from a config file, the same LOADED-not-LOCKED discipline
|
|
341
|
+
* `loadedSchemaIdentity` enforces for the migration bundle. Without the column on the row there is
|
|
342
|
+
* nothing for the host to read, and "we pushed it" would again be unfalsifiable.
|
|
343
|
+
*
|
|
344
|
+
* PK is `(team_id, slot)` — no `account_id`, because a Mirror DO **is** one account (same convention
|
|
345
|
+
* as every table here). Soft-deleted via `removed_at` so clearing a slot propagates as a change rather
|
|
346
|
+
* than as a row that silently stops being mentioned.
|
|
347
|
+
*/
|
|
348
|
+
export const team_workflow_mapping = sqliteTable(
|
|
349
|
+
"team_workflow_mapping",
|
|
350
|
+
{
|
|
351
|
+
team_id: text("team_id").notNull(),
|
|
352
|
+
/** One of the eleven Catalyst slots; the five load-bearing ones gate the fleet. */
|
|
353
|
+
slot: text("slot").notNull(),
|
|
354
|
+
/** THE AUTHORITY — joined against workflow_states.id by every consumer. */
|
|
355
|
+
linear_state_id: text("linear_state_id").notNull(),
|
|
356
|
+
linear_state_name: text("linear_state_name"),
|
|
357
|
+
linear_state_type: text("linear_state_type"),
|
|
358
|
+
/** The ACCOUNT-wide revision this row was written at. The scalar a host echoes back. */
|
|
359
|
+
workflow_rev: integer("workflow_rev"),
|
|
360
|
+
updated_at: integer("updated_at"),
|
|
361
|
+
removed_at: integer("removed_at"),
|
|
362
|
+
},
|
|
363
|
+
(t) => [primaryKey({ columns: [t.team_id, t.slot] })],
|
|
364
|
+
);
|
|
365
|
+
|
|
321
366
|
export const workflow_states = sqliteTable(
|
|
322
367
|
"workflow_states",
|
|
323
368
|
{
|
|
@@ -602,6 +647,192 @@ export const pr_events = sqliteTable(
|
|
|
602
647
|
(t) => [index("idx_pr_events_pr").on(t.repo_id, t.pr_number, t.created_at)],
|
|
603
648
|
);
|
|
604
649
|
|
|
650
|
+
/**
|
|
651
|
+
* CTC-667 — the CURRENT head of a pushed git ref, one row per `(repo_id, ref)`.
|
|
652
|
+
*
|
|
653
|
+
* ⛔ WHY THIS EXISTS. `github.push` is the orchestrator's REBASE-DETECTION signal (broker/router.mjs
|
|
654
|
+
* :1582) and the mirror did not ingest it at all, so the last GitHub smee tunnel cannot be retired
|
|
655
|
+
* without a host going blind on branch movement (CTL-1929). Live volume on mini-2 for 2026-08: 3,746
|
|
656
|
+
* push events.
|
|
657
|
+
*
|
|
658
|
+
* ⭐ CURRENT STATE, NOT AN EVENT LOG — the same modelling choice every other table here makes, and it
|
|
659
|
+
* is deliberate rather than lossy. Rebase detection asks "has this ref moved since I last looked",
|
|
660
|
+
* which a per-ref head answers exactly; the ORDERED sequence of pushes, for anyone who genuinely
|
|
661
|
+
* needs it, is already the raw passthrough feed's job (`event_log` / `/events/stream`, ADR-0017).
|
|
662
|
+
* Modelling pushes as rows-per-event would make this table unbounded on a busy repo for a question
|
|
663
|
+
* nobody in the census asks.
|
|
664
|
+
*
|
|
665
|
+
* `before`/`after` are kept even though `after` duplicates the ref head: the PAIR is what
|
|
666
|
+
* distinguishes a fast-forward from a force-push when read alongside `forced`, and a consumer that
|
|
667
|
+
* only ever sees the latest row would otherwise have to infer it.
|
|
668
|
+
*
|
|
669
|
+
* ⚠️ `created` / `deleted` are the webhook's own booleans for branch creation and deletion. A DELETED
|
|
670
|
+
* ref keeps its row with `deleted = 1` rather than being removed: "this branch is gone" is a fact a
|
|
671
|
+
* consumer needs, and a vanished row is indistinguishable from one that never existed.
|
|
672
|
+
*/
|
|
673
|
+
export const pushes = sqliteTable(
|
|
674
|
+
"pushes",
|
|
675
|
+
{
|
|
676
|
+
repo_id: text("repo_id").notNull(),
|
|
677
|
+
/** Full git ref as GitHub sends it, e.g. `refs/heads/main` — NOT shortened, so tags are unambiguous. */
|
|
678
|
+
ref: text("ref").notNull(),
|
|
679
|
+
/** The ref's head BEFORE this push (all-zero sha when the ref was just created). */
|
|
680
|
+
before: text("before"),
|
|
681
|
+
/** The ref's head AFTER this push (all-zero sha when the ref was deleted). */
|
|
682
|
+
after: text("after"),
|
|
683
|
+
/** True when the push was a force-push — the discriminator rebase detection actually keys on. */
|
|
684
|
+
forced: integer("forced"),
|
|
685
|
+
/** True when this push created the ref. */
|
|
686
|
+
created: integer("created"),
|
|
687
|
+
/** True when this push deleted the ref. */
|
|
688
|
+
deleted: integer("deleted"),
|
|
689
|
+
/** The ref this branch was created FROM, when GitHub reports one (`base_ref`); usually null. */
|
|
690
|
+
base_ref: text("base_ref"),
|
|
691
|
+
/** Who pushed — the resolvable `github:<login>` key, same convention as pr_events.actor_id. */
|
|
692
|
+
pusher_id: text("pusher_id"),
|
|
693
|
+
/** Head commit message/sha of the pushed head, when the payload carried a head_commit. */
|
|
694
|
+
head_commit_sha: text("head_commit_sha"),
|
|
695
|
+
updated_at: integer("updated_at"),
|
|
696
|
+
},
|
|
697
|
+
(t) => [primaryKey({ columns: [t.repo_id, t.ref] })],
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* CTC-667 — a GitHub DEPLOYMENT, one row per deployment id.
|
|
702
|
+
*
|
|
703
|
+
* ⛔ WHY THIS EXISTS. `github.deployment.created` is consumed by the orchestrator's deploy state
|
|
704
|
+
* machine (broker/router.mjs:1446/:1566) and the mirror ingested it not at all, so the last GitHub
|
|
705
|
+
* smee tunnel cannot be retired without `phase-monitor-deploy` going blind. Live volume on mini-2 for
|
|
706
|
+
* 2026-08: 68 `deployment.created`.
|
|
707
|
+
*
|
|
708
|
+
* ⭐ CURRENT STATE, one row per deployment — a deployment object is mutable only in trivial ways
|
|
709
|
+
* (description/payload), and its interesting movement lives entirely in its STATUSES, which get their
|
|
710
|
+
* own table below. `environment` is the field every consumer keys on.
|
|
711
|
+
*
|
|
712
|
+
* ⚠️ `sha` and `ref` are both kept and they are NOT redundant: a deployment is created against a ref
|
|
713
|
+
* (`main`, a tag, a PR head) and pinned to the sha that ref resolved to at creation. A consumer
|
|
714
|
+
* correlating a deploy with a merge commit needs the sha; one correlating it with a branch needs the
|
|
715
|
+
* ref. `task` is GitHub's own discriminator (`deploy` vs `deploy:migrations` etc.) and is what
|
|
716
|
+
* distinguishes two deployments to the same environment in the same second.
|
|
717
|
+
*/
|
|
718
|
+
export const deployments = sqliteTable(
|
|
719
|
+
"deployments",
|
|
720
|
+
{
|
|
721
|
+
/** GitHub's numeric deployment id, as text (same convention as the other GitHub id keys here). */
|
|
722
|
+
id: text("id").primaryKey(),
|
|
723
|
+
repo_id: text("repo_id").notNull(),
|
|
724
|
+
/** The git ref the deployment was created against, as sent (`main`, `refs/tags/v1`, a PR head). */
|
|
725
|
+
ref: text("ref"),
|
|
726
|
+
/** The commit sha that `ref` resolved to at creation — the correlation key for a merge commit. */
|
|
727
|
+
sha: text("sha"),
|
|
728
|
+
/** GitHub's task discriminator (`deploy`, `deploy:migrations`, …); distinguishes sibling deploys. */
|
|
729
|
+
task: text("task"),
|
|
730
|
+
/** The environment name — the field the deploy state machine keys on (`staging`, `production`). */
|
|
731
|
+
environment: text("environment"),
|
|
732
|
+
/** GitHub's own "is this the live one" flag for the environment, when the payload carries it. */
|
|
733
|
+
production_environment: integer("production_environment"),
|
|
734
|
+
/** True when GitHub marks the environment transient (a preview/PR env that will be torn down). */
|
|
735
|
+
transient_environment: integer("transient_environment"),
|
|
736
|
+
/** Human description, when supplied by whoever created the deployment. */
|
|
737
|
+
description: text("description"),
|
|
738
|
+
/** Who created it — the resolvable `github:<login>` key, same convention as pr_events.actor_id. */
|
|
739
|
+
creator_id: text("creator_id"),
|
|
740
|
+
created_at: integer("created_at"),
|
|
741
|
+
updated_at: integer("updated_at"),
|
|
742
|
+
},
|
|
743
|
+
(t) => [index("idx_deployments_repo_env").on(t.repo_id, t.environment, t.created_at)],
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* CTC-667 — a deployment's STATUS transitions, one row per status id (append-only).
|
|
748
|
+
*
|
|
749
|
+
* ⛔ WHY A SECOND TABLE RATHER THAN A `state` COLUMN ON `deployments`. The orchestrator routes on
|
|
750
|
+
* `github.deployment_status.success|failure|error` (broker/router.mjs:1456/:1570) — it acts on the
|
|
751
|
+
* ARRIVAL of a transition, not on a current value. Folding statuses into the deployment row would
|
|
752
|
+
* collapse `pending → in_progress → success` into whatever the replica happened to observe last, so
|
|
753
|
+
* two transitions landing between one host's polls would silently lose the first. GitHub's statuses
|
|
754
|
+
* are immutable and carry their own ids, which makes append-only the honest model — the same choice
|
|
755
|
+
* `pr_events` makes, and for the same reason.
|
|
756
|
+
*
|
|
757
|
+
* ⭐ AND THE VOLUME MAKES IT FREE. `deployment_status.*` ran 136 events on mini-2 for all of 2026-08.
|
|
758
|
+
* The unboundedness argument that made `pushes` current-state (3,746/month, one question) does not
|
|
759
|
+
* apply here: this table grows by low hundreds a month and answers a question about ORDER.
|
|
760
|
+
*
|
|
761
|
+
* ⚠️ `target_url` and `environment_url` are BOTH carried and are different things — `target_url` is
|
|
762
|
+
* where the deploy's LOGS are (the CI run), `environment_url` is where the deployed thing IS. The
|
|
763
|
+
* census names both because the state machine reads both; keeping only one would look complete.
|
|
764
|
+
*/
|
|
765
|
+
export const deployment_statuses = sqliteTable(
|
|
766
|
+
"deployment_statuses",
|
|
767
|
+
{
|
|
768
|
+
/** GitHub's numeric deployment-status id, as text. Immutable — a status is never rewritten. */
|
|
769
|
+
id: text("id").primaryKey(),
|
|
770
|
+
repo_id: text("repo_id").notNull(),
|
|
771
|
+
/** FK to `deployments.id`. Kept as text for the same reason `id` is. */
|
|
772
|
+
deployment_id: text("deployment_id").notNull(),
|
|
773
|
+
/** pending | queued | in_progress | success | failure | error | inactive — GitHub's own vocabulary. */
|
|
774
|
+
state: text("state"),
|
|
775
|
+
/** The environment, denormalized from the status (GitHub sends it) so a reader needs no join. */
|
|
776
|
+
environment: text("environment"),
|
|
777
|
+
/** Where the deploy's LOGS are — the CI run URL. */
|
|
778
|
+
target_url: text("target_url"),
|
|
779
|
+
/** Where the deployed thing IS — the live URL for this environment. */
|
|
780
|
+
environment_url: text("environment_url"),
|
|
781
|
+
description: text("description"),
|
|
782
|
+
/** Who/what reported the status — the resolvable `github:<login>` key. */
|
|
783
|
+
creator_id: text("creator_id"),
|
|
784
|
+
created_at: integer("created_at"),
|
|
785
|
+
updated_at: integer("updated_at"),
|
|
786
|
+
},
|
|
787
|
+
(t) => [index("idx_deployment_statuses_deployment").on(t.deployment_id, t.created_at)],
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* CTC-667 — a PR review THREAD's resolution state, one row per thread.
|
|
792
|
+
*
|
|
793
|
+
* ⛔ WHY THIS EXISTS, and it is a merge gate rather than a nicety. AGENTS.md makes "every review
|
|
794
|
+
* thread has been addressed and resolved" a condition for merging, the orchestrator routes on
|
|
795
|
+
* `github.pr_review_thread.resolved` (broker/router.mjs:1562), and `pr_review_comments` has NO
|
|
796
|
+
* resolution column — so with the smee tunnel gone, thread resolution is not merely stale, it is
|
|
797
|
+
* UNRECONSTRUCTABLE from the replica. Live volume on mini-2 for 2026-08: 2,111 resolutions.
|
|
798
|
+
*
|
|
799
|
+
* ⭐ CURRENT STATE, one row per thread, and here that IS the question: the gate asks "is this thread
|
|
800
|
+
* resolved NOW", not "how many times was it toggled". A thread that is resolved, reopened and
|
|
801
|
+
* resolved again ends in the same place a consumer needs to read.
|
|
802
|
+
*
|
|
803
|
+
* ⚠️ `id` IS GITHUB'S GRAPHQL `node_id`, NOT A NUMERIC REST ID — the `pull_request_review_thread`
|
|
804
|
+
* webhook's `thread` object carries no numeric id at all. This is the only GitHub table here whose
|
|
805
|
+
* key is a node id, which is why it is said out loud: a reader assuming the numeric convention would
|
|
806
|
+
* write a joiner that never matches.
|
|
807
|
+
*
|
|
808
|
+
* ⚠️ `first_comment_id` is the anchor INTO `pr_review_comments` (the earliest comment the thread's
|
|
809
|
+
* payload carries), because the reverse link does not exist — the `pull_request_review_comment`
|
|
810
|
+
* webhook carries no thread id, so a comment cannot name its thread. Nullable: a thread payload
|
|
811
|
+
* arriving with an empty `comments` array is not an error, it is a thread whose comments were
|
|
812
|
+
* deleted.
|
|
813
|
+
*/
|
|
814
|
+
export const pr_review_threads = sqliteTable(
|
|
815
|
+
"pr_review_threads",
|
|
816
|
+
{
|
|
817
|
+
/** GitHub's GraphQL `node_id` for the thread — NOT a numeric REST id (see the doc above). */
|
|
818
|
+
id: text("id").primaryKey(),
|
|
819
|
+
repo_id: text("repo_id").notNull(),
|
|
820
|
+
pr_number: integer("pr_number").notNull(),
|
|
821
|
+
/** 1 resolved / 0 unresolved / null unknown — never coerce an absent action to "unresolved". */
|
|
822
|
+
resolved: integer("resolved"),
|
|
823
|
+
/** When it was last resolved (ms epoch); null while unresolved or never resolved. */
|
|
824
|
+
resolved_at: integer("resolved_at"),
|
|
825
|
+
/** Who resolved it — the resolvable `github:<login>` key; null when unresolved. */
|
|
826
|
+
resolver_id: text("resolver_id"),
|
|
827
|
+
/** The earliest comment id in the thread — the anchor into pr_review_comments (see doc). */
|
|
828
|
+
first_comment_id: text("first_comment_id"),
|
|
829
|
+
/** How many comments the thread's payload carried, so "empty thread" is distinguishable from null. */
|
|
830
|
+
comment_count: integer("comment_count"),
|
|
831
|
+
updated_at: integer("updated_at"),
|
|
832
|
+
},
|
|
833
|
+
(t) => [index("idx_pr_review_threads_pr").on(t.repo_id, t.pr_number)],
|
|
834
|
+
);
|
|
835
|
+
|
|
605
836
|
// ── Mirror infra (not domain entities — never ride the change-feed) ─────────────────────────────────
|
|
606
837
|
|
|
607
838
|
export const processed_events = sqliteTable("processed_events", {
|