@catalyst-cloud/schema 0.1.13 → 0.1.15

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catalyst-cloud/schema",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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,
@@ -41,6 +43,7 @@ import {
41
43
  pr_commits,
42
44
  pr_events,
43
45
  pr_ancillary_backfill,
46
+ push_subscriptions,
44
47
  } from "./mirror.js";
45
48
 
46
49
  export * from "./mirror.js";
@@ -78,6 +81,8 @@ export const mirrorSchema = {
78
81
  projects,
79
82
  cycles,
80
83
  workflow_states,
84
+ // CTC-624: projected from the D1 registry, the one mirror table whose authority is not a provider.
85
+ team_workflow_mapping,
81
86
  initiatives,
82
87
  project_initiatives,
83
88
  comments,
@@ -112,6 +117,9 @@ export const mirrorSchema = {
112
117
  pr_commits,
113
118
  pr_events,
114
119
  pr_ancillary_backfill,
120
+ // CTC-619: a member's Web Push subscription. Rides the change feed like any other entity — a
121
+ // member-facing settings UI can reflect subscribe/unsubscribe reactively.
122
+ push_subscriptions,
115
123
  } as const;
116
124
 
117
125
  /**
@@ -139,6 +147,9 @@ export const mirrorEntityTables = {
139
147
  projects,
140
148
  cycles,
141
149
  workflow_states,
150
+ // CTC-624: the tenant's per-team workflow decision. On this plane deliberately — it is how
151
+ // anything reaches a host at all, and the browser board is a legitimate consumer (ADR-0033).
152
+ team_workflow_mapping,
142
153
  initiatives,
143
154
  project_initiatives,
144
155
  comments,
@@ -157,6 +168,8 @@ export const mirrorEntityTables = {
157
168
  pr_conversation_comments,
158
169
  pr_commits,
159
170
  pr_events,
171
+ // CTC-619: a member's Web Push subscription.
172
+ push_subscriptions,
160
173
  } as const;
161
174
 
162
175
  /** The wire `entity` name of a change-feed-carried table (keys of mirrorEntityTables). */
@@ -172,6 +172,20 @@ export const MIRROR_MIGRATIONS = {
172
172
  tag: "0022_chilly_gorgon",
173
173
  breakpoints: true,
174
174
  },
175
+ {
176
+ idx: 23,
177
+ version: "6",
178
+ when: 1786935258669,
179
+ tag: "0023_ambiguous_micromax",
180
+ breakpoints: true,
181
+ },
182
+ {
183
+ idx: 24,
184
+ version: "6",
185
+ when: 1786984427741,
186
+ tag: "0024_long_spencer_smythe",
187
+ breakpoints: true,
188
+ },
175
189
  ],
176
190
  },
177
191
  migrations: {
@@ -219,5 +233,9 @@ export const MIRROR_MIGRATIONS = {
219
233
  "CREATE TABLE `pr_ancillary_backfill` (\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`backfilled_at` integer NOT NULL,\n\tPRIMARY KEY(`repo_id`, `pr_number`)\n);\n--> statement-breakpoint\nCREATE TABLE `pr_commits` (\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`sha` text NOT NULL,\n\t`message` text,\n\t`author_login` text,\n\t`author_name` text,\n\t`author_email` text,\n\t`authored_at` integer,\n\t`committed_at` integer,\n\tPRIMARY KEY(`repo_id`, `pr_number`, `sha`)\n);\n--> statement-breakpoint\nCREATE TABLE `pr_conversation_comments` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`author_id` text,\n\t`body` text,\n\t`created_at` integer,\n\t`updated_at` integer,\n\t`removed_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_pr_conversation_comments_pr` ON `pr_conversation_comments` (`repo_id`,`pr_number`);--> statement-breakpoint\nCREATE TABLE `pr_events` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`action` text,\n\t`actor_id` text,\n\t`created_at` integer,\n\t`updated_at` integer,\n\t`raw` text\n);\n--> statement-breakpoint\nCREATE INDEX `idx_pr_events_pr` ON `pr_events` (`repo_id`,`pr_number`,`created_at`);--> statement-breakpoint\nCREATE TABLE `pr_review_comments` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`pr_number` integer NOT NULL,\n\t`review_id` text,\n\t`commit_id` text,\n\t`path` text,\n\t`line` integer,\n\t`diff_hunk` text,\n\t`in_reply_to_id` text,\n\t`author_id` text,\n\t`body` text,\n\t`created_at` integer,\n\t`updated_at` integer,\n\t`removed_at` integer\n);\n--> statement-breakpoint\nCREATE INDEX `idx_pr_review_comments_pr` ON `pr_review_comments` (`repo_id`,`pr_number`);",
220
234
  "0022_chilly_gorgon":
221
235
  "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;",
236
+ "0023_ambiguous_micromax":
237
+ "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`);",
238
+ "0024_long_spencer_smythe":
239
+ "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",
222
240
  },
223
241
  } 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
  {
@@ -851,3 +896,25 @@ export const pr_ancillary_backfill = sqliteTable(
851
896
  },
852
897
  (t) => [primaryKey({ columns: [t.repo_id, t.pr_number] })],
853
898
  );
899
+
900
+ // CTC-619 — a member's Web Push subscription (one browser/device). PK is the subscription `endpoint`
901
+ // itself — a push endpoint URL is unique per browser install by construction, so keying on it is what
902
+ // makes "a duplicate subscribe for the same endpoint does not create a second row" (the ticket's own
903
+ // AC) a plain upsert, no synthetic id needed. `member_id` is the Linear user id (FK-shaped to
904
+ // users.id, same as issues.assignee_id) — the ingest trigger's own correlation key. HARD delete on
905
+ // unsubscribe (no removed_at): a stale/revoked push subscription has no historical value, matching
906
+ // `users`' own "HARD-UPSERT, no soft delete" precedent for similarly overwrite-in-place data.
907
+ export const push_subscriptions = sqliteTable(
908
+ "push_subscriptions",
909
+ {
910
+ endpoint: text("endpoint").primaryKey(),
911
+ member_id: text("member_id").notNull(),
912
+ p256dh: text("p256dh").notNull(),
913
+ auth: text("auth").notNull(),
914
+ created_at: integer("created_at"),
915
+ last_used_at: integer("last_used_at"),
916
+ },
917
+ // The ingest trigger's own read pattern: "every subscription for this member" (member_id, NOT the
918
+ // PK), keyed off issues.assignee_id — the same shape idx_workflow_states_team indexes on team_id.
919
+ (t) => [index("idx_push_subscriptions_member").on(t.member_id)],
920
+ );