@catalyst-cloud/schema 0.1.1 → 0.1.2

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.
@@ -15,14 +15,22 @@
15
15
  // • `relations` join on issues.identifier (issue_identifier), NOT issues.id.
16
16
  // • `check_runs` join a PR by head_sha, NOT PR number.
17
17
  // • PR mergeable / mergeable_state are nullable until a reconcile resolves them.
18
+
18
19
  import { sqliteTable, text, integer, real, primaryKey, index } from "drizzle-orm/sqlite-core";
20
+
19
21
  // ── Linear ───────────────────────────────────────────────────────────────────────────────────────
20
- export const issues = sqliteTable("issues", {
22
+
23
+ export const issues = sqliteTable(
24
+ "issues",
25
+ {
21
26
  id: text("id").primaryKey(),
22
27
  identifier: text("identifier"),
23
28
  title: text("title"),
24
29
  description: text("description"),
25
30
  state: text("state"),
31
+ // CTC-148: the workflow state id behind the `state` NAME — lets a consumer key on state without
32
+ // string-matching the renameable name.
33
+ state_id: text("state_id"),
26
34
  assignee: text("assignee"),
27
35
  // CTC-USERS: the assignee's user id (FK → users.id), so the row JOINs to a name + avatar. The
28
36
  // legacy `assignee` name string is KEPT for back-compat (the SSE list row still carries it).
@@ -32,6 +40,10 @@ export const issues = sqliteTable("issues", {
32
40
  project_id: text("project_id"),
33
41
  cycle_id: text("cycle_id"),
34
42
  team_id: text("team_id"),
43
+ // CTC-148: denormalized team key + name (off the issue's `team` ref) so a row labels its team
44
+ // ("ENG / Engineering") without a teams lookup. team_id is KEPT.
45
+ team_key: text("team_key"),
46
+ team_name: text("team_name"),
35
47
  delegate_id: text("delegate_id"),
36
48
  delegate_name: text("delegate_name"),
37
49
  bot_actor_name: text("bot_actor_name"),
@@ -57,7 +69,8 @@ export const issues = sqliteTable("issues", {
57
69
  archived_at: integer("archived_at"),
58
70
  removed_at: integer("removed_at"),
59
71
  raw: text("raw"),
60
- }, (t) => [
72
+ },
73
+ (t) => [
61
74
  index("idx_issues_team").on(t.team_id),
62
75
  // CTC-59: the list query — `WHERE removed_at IS NULL ORDER BY updated_at DESC` (buildIssuesView).
63
76
  // The composite lets SQLite seek the live rows then walk updated_at in order (no temp b-tree).
@@ -68,14 +81,17 @@ export const issues = sqliteTable("issues", {
68
81
  // Drizzle SSOT ONLY (NOT schema.sql) — migrate.ts swallows a duplicate CREATE INDEX on replay
69
82
  // (CTC-59), so the generated `CREATE INDEX` is replay-safe; schema.sql would break the adoption test.
70
83
  index("idx_issues_sort_order").on(t.sort_order),
71
- ]);
84
+ ],
85
+ );
86
+
72
87
  export const labels = sqliteTable("labels", {
73
- id: text("id").primaryKey(),
74
- name: text("name"),
75
- color: text("color"),
76
- updated_at: integer("updated_at"),
77
- removed_at: integer("removed_at"),
88
+ id: text("id").primaryKey(),
89
+ name: text("name"),
90
+ color: text("color"),
91
+ updated_at: integer("updated_at"),
92
+ removed_at: integer("removed_at"),
78
93
  });
94
+
79
95
  // Teammate identities (CTC-USERS): resolves issue assignees + delegate AGENTS (and comment/PR
80
96
  // authors/reviewers) to a name + avatar so the UI never renders a gray "Unknown" chip. AGENT
81
97
  // identities are first-class: `type` is human | agent | bot | app. Populated three ways — a ROSTER
@@ -86,27 +102,38 @@ export const labels = sqliteTable("labels", {
86
102
  // in place, never tombstoned (an avatar/name that goes missing just stops being referenced). `id` is
87
103
  // the source-native id: Linear's user node id, the bot actor id, or a `github:<login>` synthetic id.
88
104
  export const users = sqliteTable("users", {
89
- id: text("id").primaryKey(),
90
- name: text("name"),
91
- display_name: text("display_name"),
92
- avatar_url: text("avatar_url"),
93
- type: text("type"), // human | agent | bot | app
94
- source: text("source"), // linear | github
95
- updated_at: integer("updated_at"),
105
+ id: text("id").primaryKey(),
106
+ name: text("name"),
107
+ display_name: text("display_name"),
108
+ avatar_url: text("avatar_url"),
109
+ type: text("type"), // human | agent | bot | app
110
+ source: text("source"), // linear | github
111
+ updated_at: integer("updated_at"),
96
112
  });
97
- export const issue_labels = sqliteTable("issue_labels", {
113
+
114
+ export const issue_labels = sqliteTable(
115
+ "issue_labels",
116
+ {
98
117
  issue_id: text("issue_id").notNull(),
99
118
  label_id: text("label_id").notNull(),
100
- }, (t) => [primaryKey({ columns: [t.issue_id, t.label_id] })]);
119
+ },
120
+ (t) => [primaryKey({ columns: [t.issue_id, t.label_id] })],
121
+ );
122
+
101
123
  // FULL-poll-and-diff every reconcile pass (Linear fires NO issueRelations webhooks). Delete-absent is
102
124
  // load-bearing — and there is intentionally NO removed_at: a stale edge is HARD-deleted, never soft.
103
- export const relations = sqliteTable("relations", {
125
+ export const relations = sqliteTable(
126
+ "relations",
127
+ {
104
128
  id: text("id").primaryKey(),
105
129
  type: text("type"), // blocks | duplicate | related
106
130
  issue_identifier: text("issue_identifier"),
107
131
  related_identifier: text("related_identifier"),
108
132
  updated_at: integer("updated_at"),
109
- }, (t) => [index("idx_relations_issue").on(t.issue_identifier)]);
133
+ },
134
+ (t) => [index("idx_relations_issue").on(t.issue_identifier)],
135
+ );
136
+
110
137
  // Linear IssueHistory events (CTC-14) — the per-issue change timeline. Captured INLINE on the issues
111
138
  // delta sweep (a nested history(last:100) connection), NOT a separate leg and NOT a webhook path. Each
112
139
  // node is IMMUTABLE with a stable `id`; HARD upsert keyed on `id` (NO removed_at — a history row never
@@ -115,7 +142,9 @@ export const relations = sqliteTable("relations", {
115
142
  // name — no states table). from_due_date/to_due_date are TimelessDate "YYYY-MM-DD" stored AS-IS as
116
143
  // TEXT (the trap), never a timestamp. added_label_ids/removed_label_ids are JSON array strings; the
117
144
  // boolean columns are the mirror's 0/1 ints. raw is the full node JSON (full fidelity / forward-compat).
118
- export const issue_history = sqliteTable("issue_history", {
145
+ export const issue_history = sqliteTable(
146
+ "issue_history",
147
+ {
119
148
  id: text("id").primaryKey(),
120
149
  issue_id: text("issue_id").notNull(), // FK → issues.id
121
150
  actor_id: text("actor_id"), // FK → users.id; nullable (system events have no actor)
@@ -149,67 +178,79 @@ export const issue_history = sqliteTable("issue_history", {
149
178
  auto_closed: integer("auto_closed"), // 0/1
150
179
  trashed: integer("trashed"), // 0/1
151
180
  raw: text("raw"), // JSON.stringify(node) — full fidelity / forward-compat (like issues.raw)
152
- },
153
- // The timeline index lives in the Drizzle SSOT ONLY (NOT schema.sql) — migrate.ts makes the generated
154
- // CREATE INDEX replay-safe (same as the issues read-path indexes); putting it in schema.sql would
155
- // break the adoption test.
156
- (t) => [index("idx_issue_history_issue_created").on(t.issue_id, t.created_at)]);
181
+ },
182
+ // The timeline index lives in the Drizzle SSOT ONLY (NOT schema.sql) — migrate.ts makes the generated
183
+ // CREATE INDEX replay-safe (same as the issues read-path indexes); putting it in schema.sql would
184
+ // break the adoption test.
185
+ (t) => [index("idx_issue_history_issue_created").on(t.issue_id, t.created_at)],
186
+ );
187
+
157
188
  // CTC-HIERARCHY enriches the project row with the lead (FK → users.id), description, lifecycle dates,
158
189
  // progress, health, and presentation (icon/color). `target_date` / `start_date` are Linear
159
190
  // TimelessDates ("YYYY-MM-DD") — stored AS-IS as TEXT, NEVER through isoToMs (the same trap as
160
191
  // issues.due_date). `progress` is a Float (0..1).
161
192
  export const projects = sqliteTable("projects", {
162
- id: text("id").primaryKey(),
163
- name: text("name"),
164
- state: text("state"),
165
- description: text("description"),
166
- lead_id: text("lead_id"), // FK → users.id (CTC-HIERARCHY); upserted from the project lead.
167
- target_date: text("target_date"), // TimelessDate "YYYY-MM-DD" — TEXT AS-IS (the trap), not a ts.
168
- start_date: text("start_date"), // TimelessDate "YYYY-MM-DD" — TEXT AS-IS (the trap), not a ts.
169
- progress: real("progress"),
170
- health: text("health"), // onTrack | atRisk | offTrack (null until a project update reports it).
171
- icon: text("icon"),
172
- color: text("color"),
173
- updated_at: integer("updated_at"),
174
- removed_at: integer("removed_at"),
193
+ id: text("id").primaryKey(),
194
+ name: text("name"),
195
+ state: text("state"),
196
+ description: text("description"),
197
+ lead_id: text("lead_id"), // FK → users.id (CTC-HIERARCHY); upserted from the project lead.
198
+ target_date: text("target_date"), // TimelessDate "YYYY-MM-DD" — TEXT AS-IS (the trap), not a ts.
199
+ start_date: text("start_date"), // TimelessDate "YYYY-MM-DD" — TEXT AS-IS (the trap), not a ts.
200
+ progress: real("progress"),
201
+ health: text("health"), // onTrack | atRisk | offTrack (null until a project update reports it).
202
+ icon: text("icon"),
203
+ color: text("color"),
204
+ updated_at: integer("updated_at"),
205
+ removed_at: integer("removed_at"),
175
206
  });
207
+
176
208
  // CTC-HIERARCHY enriches the cycle row with its display name + progress + current scope/issue counts.
177
209
  // Linear exposes scope/issue counts only as weekly history arrays (completedScopeHistory etc.); we
178
210
  // store the CURRENT value (the last array element) as a scalar — not the whole history.
179
211
  export const cycles = sqliteTable("cycles", {
180
- id: text("id").primaryKey(),
181
- number: integer("number"),
182
- name: text("name"),
183
- progress: real("progress"),
184
- completed_scope: real("completed_scope"), // last(completedScopeHistory) — current completed points.
185
- scope: real("scope"), // last(scopeHistory) — current total scope points.
186
- completed_issue_count: integer("completed_issue_count"), // last(completedIssueCountHistory).
187
- issue_count: integer("issue_count"), // last(issueCountHistory).
188
- starts_at: integer("starts_at"),
189
- ends_at: integer("ends_at"),
190
- updated_at: integer("updated_at"),
212
+ id: text("id").primaryKey(),
213
+ number: integer("number"),
214
+ name: text("name"),
215
+ progress: real("progress"),
216
+ completed_scope: real("completed_scope"), // last(completedScopeHistory) — current completed points.
217
+ scope: real("scope"), // last(scopeHistory) — current total scope points.
218
+ completed_issue_count: integer("completed_issue_count"), // last(completedIssueCountHistory).
219
+ issue_count: integer("issue_count"), // last(issueCountHistory).
220
+ starts_at: integer("starts_at"),
221
+ ends_at: integer("ends_at"),
222
+ updated_at: integer("updated_at"),
191
223
  });
224
+
192
225
  // Linear initiatives (CTC-HIERARCHY) — the top of the HIERARCHY (initiative → project → cycle →
193
226
  // issue). PK `id` (Linear node id). `status` is the InitiativeStatus enum string; `target_date` is a
194
227
  // TimelessDate ("YYYY-MM-DD") stored AS-IS as TEXT (the trap). `owner_id` is the FK → users.id.
195
228
  // HARD upsert — NO removed_at (a vanished initiative just stops being referenced by the join table).
196
229
  export const initiatives = sqliteTable("initiatives", {
197
- id: text("id").primaryKey(),
198
- name: text("name"),
199
- description: text("description"),
200
- status: text("status"),
201
- target_date: text("target_date"), // TimelessDate "YYYY-MM-DD" — TEXT AS-IS (the trap), not a ts.
202
- owner_id: text("owner_id"), // FK → users.id (CTC-HIERARCHY); upserted from the initiative owner.
203
- updated_at: integer("updated_at"),
230
+ id: text("id").primaryKey(),
231
+ name: text("name"),
232
+ description: text("description"),
233
+ status: text("status"),
234
+ target_date: text("target_date"), // TimelessDate "YYYY-MM-DD" — TEXT AS-IS (the trap), not a ts.
235
+ owner_id: text("owner_id"), // FK → users.id (CTC-HIERARCHY); upserted from the initiative owner.
236
+ updated_at: integer("updated_at"),
204
237
  });
238
+
205
239
  // project↔initiative join (CTC-HIERARCHY) — Linear models this as many:many (a project belongs to
206
240
  // many initiatives), so it's an edge table modeled on issue_labels: composite PK, no updated_at, full
207
241
  // poll-and-diff per project leg in the reconcile (Linear fires no membership webhook for it).
208
- export const project_initiatives = sqliteTable("project_initiatives", {
242
+ export const project_initiatives = sqliteTable(
243
+ "project_initiatives",
244
+ {
209
245
  project_id: text("project_id").notNull(),
210
246
  initiative_id: text("initiative_id").notNull(),
211
- }, (t) => [primaryKey({ columns: [t.project_id, t.initiative_id] })]);
212
- export const comments = sqliteTable("comments", {
247
+ },
248
+ (t) => [primaryKey({ columns: [t.project_id, t.initiative_id] })],
249
+ );
250
+
251
+ export const comments = sqliteTable(
252
+ "comments",
253
+ {
213
254
  id: text("id").primaryKey(),
214
255
  issue_id: text("issue_id"),
215
256
  body: text("body"),
@@ -222,12 +263,17 @@ export const comments = sqliteTable("comments", {
222
263
  created_at: integer("created_at"),
223
264
  updated_at: integer("updated_at"),
224
265
  removed_at: integer("removed_at"),
225
- },
226
- // CTC-59: the detail comments fetch — `WHERE issue_id = ? AND removed_at IS NULL ORDER BY updated_at`.
227
- // Composite covers the filter AND the sort, so SQLite seeks instead of building a temp b-tree.
228
- (t) => [index("idx_comments_issue").on(t.issue_id, t.removed_at, t.updated_at)]);
266
+ },
267
+ // CTC-59: the detail comments fetch — `WHERE issue_id = ? AND removed_at IS NULL ORDER BY updated_at`.
268
+ // Composite covers the filter AND the sort, so SQLite seeks instead of building a temp b-tree.
269
+ (t) => [index("idx_comments_issue").on(t.issue_id, t.removed_at, t.updated_at)],
270
+ );
271
+
229
272
  // ── GitHub ───────────────────────────────────────────────────────────────────────────────────────
230
- export const pull_requests = sqliteTable("pull_requests", {
273
+
274
+ export const pull_requests = sqliteTable(
275
+ "pull_requests",
276
+ {
231
277
  repo_id: text("repo_id").notNull(),
232
278
  number: integer("number").notNull(),
233
279
  node_id: text("node_id"),
@@ -254,12 +300,17 @@ export const pull_requests = sqliteTable("pull_requests", {
254
300
  milestone_title: text("milestone_title"),
255
301
  updated_at: integer("updated_at"),
256
302
  synced_at: integer("synced_at"),
257
- }, (t) => [
303
+ },
304
+ (t) => [
258
305
  primaryKey({ columns: [t.repo_id, t.number] }),
259
306
  // CTC-59: the pulls list — `ORDER BY updated_at DESC` (buildPullsView).
260
307
  index("idx_pulls_updated").on(t.updated_at),
261
- ]);
262
- export const check_runs = sqliteTable("check_runs", {
308
+ ],
309
+ );
310
+
311
+ export const check_runs = sqliteTable(
312
+ "check_runs",
313
+ {
263
314
  repo_id: text("repo_id").notNull(),
264
315
  check_run_id: text("check_run_id").primaryKey(),
265
316
  head_sha: text("head_sha"),
@@ -268,41 +319,57 @@ export const check_runs = sqliteTable("check_runs", {
268
319
  conclusion: text("conclusion"),
269
320
  check_suite_id: text("check_suite_id"),
270
321
  updated_at: integer("updated_at"),
271
- }, (t) => [index("idx_check_runs_sha").on(t.head_sha)]);
272
- export const commit_statuses = sqliteTable("commit_statuses", {
322
+ },
323
+ (t) => [index("idx_check_runs_sha").on(t.head_sha)],
324
+ );
325
+
326
+ export const commit_statuses = sqliteTable(
327
+ "commit_statuses",
328
+ {
273
329
  repo_id: text("repo_id").notNull(),
274
330
  sha: text("sha").notNull(),
275
331
  context: text("context").notNull(),
276
332
  state: text("state"),
277
333
  target_url: text("target_url"),
278
334
  updated_at: integer("updated_at"),
279
- }, (t) => [primaryKey({ columns: [t.repo_id, t.sha, t.context] })]);
335
+ },
336
+ (t) => [primaryKey({ columns: [t.repo_id, t.sha, t.context] })],
337
+ );
338
+
280
339
  export const reviews = sqliteTable("reviews", {
281
- repo_id: text("repo_id").notNull(),
282
- pr_number: integer("pr_number").notNull(),
283
- review_id: text("review_id").primaryKey(),
284
- user_id: text("user_id"),
285
- state: text("state"),
286
- submitted_at: integer("submitted_at"),
340
+ repo_id: text("repo_id").notNull(),
341
+ pr_number: integer("pr_number").notNull(),
342
+ review_id: text("review_id").primaryKey(),
343
+ user_id: text("user_id"),
344
+ state: text("state"),
345
+ submitted_at: integer("submitted_at"),
287
346
  });
347
+
288
348
  // ── Mirror infra (not domain entities — never ride the change-feed) ─────────────────────────────────
349
+
289
350
  export const processed_events = sqliteTable("processed_events", {
290
- delivery_id: text("delivery_id").primaryKey(),
291
- source: text("source"),
292
- event_type: text("event_type"),
293
- received_at: integer("received_at"),
351
+ delivery_id: text("delivery_id").primaryKey(),
352
+ source: text("source"),
353
+ event_type: text("event_type"),
354
+ received_at: integer("received_at"),
294
355
  });
356
+
295
357
  export const change_log = sqliteTable("change_log", {
296
- seq: integer("seq").primaryKey({ autoIncrement: true }),
297
- entity: text("entity"),
298
- entity_id: text("entity_id"),
299
- op: text("op"), // upsert | delete
300
- payload: text("payload"),
301
- ts: integer("ts"),
358
+ seq: integer("seq").primaryKey({ autoIncrement: true }),
359
+ entity: text("entity"),
360
+ entity_id: text("entity_id"),
361
+ op: text("op"), // upsert | delete
362
+ payload: text("payload"),
363
+ ts: integer("ts"),
302
364
  });
303
- export const sync_cursors = sqliteTable("sync_cursors", {
365
+
366
+ export const sync_cursors = sqliteTable(
367
+ "sync_cursors",
368
+ {
304
369
  source: text("source").notNull(),
305
370
  entity: text("entity").notNull(),
306
371
  cursor: text("cursor"),
307
372
  last_run_at: integer("last_run_at"),
308
- }, (t) => [primaryKey({ columns: [t.source, t.entity] })]);
373
+ },
374
+ (t) => [primaryKey({ columns: [t.source, t.entity] })],
375
+ );