@catalyst-cloud/sdk 0.14.2 → 0.16.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.
package/dist/node.js CHANGED
@@ -5073,10 +5073,18 @@ var currentStateEventTypes = {
5073
5073
  }
5074
5074
  };
5075
5075
  var telemetryEventTypes = {
5076
+ "catalyst.mirror.request.summary": {
5077
+ reason: "CTC-4063 \u2014 one request per route and verified caller. Sampling would discard the distribution required for per-key usage.",
5078
+ sampling: { kind: "always" }
5079
+ },
5076
5080
  "telemetry.ingest.tenant-forged": {
5077
5081
  reason: "CTC-2788 \u2014 a gateway payload named a tenant other than its token's on `account_id` or `catalyst.tenant`, and the gateway overwrote it. Rare by construction: a current runner image emits `catalyst.tenant` equal to its own tenant, so this fires only on a stale or wrong token, or on a deliberate forgery. Each occurrence is independently actionable (which tenant held the token, which tenant was claimed, which signal), so it is never sampled away.",
5078
5082
  sampling: { kind: "always" }
5079
5083
  },
5084
+ "telemetry.ingest.scope-forged": {
5085
+ reason: "CTC-2789 \u2014 a gateway payload claimed `catalyst.scope` itself (e.g. `platform`), and the gateway overwrote it to `tenant`, the only scope a tenant credential can ever write. Rare by construction: no producer in this repo sets the attribute, so this fires only on a stale image or a deliberate forgery. Each occurrence is independently actionable (which tenant held the token, which scope was claimed, which signal), so it is never sampled away.",
5086
+ sampling: { kind: "always" }
5087
+ },
5080
5088
  "account.ratelimit.sampled": {
5081
5089
  reason: "A periodic provider rate-limit budget reading \u2014 a gauge, not a state transition."
5082
5090
  },
@@ -5721,6 +5729,18 @@ function stateBucketClause(bucket) {
5721
5729
  }
5722
5730
  var STATE_KEY_SQL = stateKeySql("issues");
5723
5731
  var DONE_KEY_PLACEHOLDERS = DONE_STATE_KEYS.map(() => "?").join(", ");
5732
+ function openStateLiteralSql(stateColumn) {
5733
+ const literals = DONE_STATE_KEYS.map((key) => `'${key.replace(/'/g, "''")}'`).join(", ");
5734
+ return `replace(lower(trim(${stateColumn})), ' ', '_') NOT IN (${literals})`;
5735
+ }
5736
+ var OPEN_ASSIGNEE_INDEX_WHERE = `removed_at IS NULL AND ${openStateLiteralSql("state")}`;
5737
+ function whereHash(text2) {
5738
+ let hash = 2166136261;
5739
+ for (let i = 0; i < text2.length; i++)
5740
+ hash = Math.imul(hash ^ text2.charCodeAt(i), 16777619) >>> 0;
5741
+ return hash.toString(16).padStart(8, "0");
5742
+ }
5743
+ var OPEN_ASSIGNEE_INDEX_NAME = `idx_issues_open_assignee_${whereHash(OPEN_ASSIGNEE_INDEX_WHERE)}`;
5724
5744
  var OPEN_BLOCKED_TARGETS_SQL = `FROM relations r WHERE r.issue_identifier = issues.identifier AND r.type = 'blocks' AND NOT EXISTS (SELECT 1 FROM issues ri WHERE ri.identifier = r.related_identifier AND ri.removed_at IS NULL AND ${stateKeySql("ri")} IN (${DONE_KEY_PLACEHOLDERS}))`;
5725
5745
  function blockedOpenCountSql() {
5726
5746
  return `(SELECT COUNT(*) ${OPEN_BLOCKED_TARGETS_SQL})`;
@@ -5731,7 +5751,14 @@ function waitingOnMeClause(linearUserId, mode, carried = null) {
5731
5751
  const notDone = `${STATE_KEY_SQL} NOT IN (${DONE_KEY_PLACEHOLDERS})`;
5732
5752
  const declared = `(issues.assignee_id = ? AND ${notDone} AND EXISTS (SELECT 1 FROM issue_labels il JOIN labels l ON l.id = il.label_id WHERE il.issue_id = issues.id AND (l.name = ? OR substr(l.name, 1, 4) = ?) AND l.removed_at IS NULL))`;
5733
5753
  const declaredBindings = [linearUserId, ...DONE_STATE_KEYS, ASK_LABEL, ASK_LABEL_PREFIX];
5734
- if (mode === "asks") return { sql: ` AND ${declared}`, bindings: declaredBindings };
5754
+ const openForMember = ` AND issues.assignee_id = ? AND ${openStateLiteralSql("issues.state")}`;
5755
+ const notDecidedAsk = ` AND NOT (EXISTS (SELECT 1 FROM issue_labels dil JOIN labels dl ON dl.id = dil.label_id WHERE dil.issue_id = issues.id AND (dl.name = ? OR substr(dl.name, 1, 4) = ?) AND dl.removed_at IS NULL) AND EXISTS (SELECT 1 FROM comments dc WHERE dc.issue_id = issues.id AND dc.removed_at IS NULL AND dc.body LIKE 'DECIDED:%'))`;
5756
+ const notDecidedAskBindings = [ASK_LABEL, ASK_LABEL_PREFIX];
5757
+ if (mode === "asks")
5758
+ return {
5759
+ sql: `${openForMember}${notDecidedAsk} AND ${declared}`,
5760
+ bindings: [linearUserId, ...notDecidedAskBindings, ...declaredBindings]
5761
+ };
5735
5762
  const blocksSomethingUnfinished = `EXISTS (SELECT 1 ${OPEN_BLOCKED_TARGETS_SQL})`;
5736
5763
  const notAlreadyAsked = (
5737
5764
  // ⛔ CTC-4306 — `CROSS JOIN`, NOT `JOIN`: SQLite never reorders a CROSS JOIN, so `ar` stays the
@@ -5755,11 +5782,14 @@ function waitingOnMeClause(linearUserId, mode, carried = null) {
5755
5782
  }
5756
5783
  const inferred = `(issues.assignee_id = ? AND issues.delegate_id IS NULL AND ${notDone} AND ${notBacklogDeferred} AND ${blocksSomethingUnfinished} AND ${notAlreadyAsked}${notCarried})`;
5757
5784
  return {
5758
- sql: ` AND (${declared} OR ${inferred})`,
5785
+ sql: `${openForMember}${notDecidedAsk} AND (${declared} OR ${inferred})`,
5759
5786
  // ⚠️ `DONE_STATE_KEYS` THREE TIMES — this row's own `notDone`, the far-end `IN` list inside the
5760
5787
  // blocking-edge NOT EXISTS, and the covering-ask's own done-list. A missed copy shifts every
5761
- // parameter after it.
5788
+ // parameter after it. The first binding is `openForMember`'s member id (CTC-4505), then the
5789
+ // decided-ask label family (CTC-5111).
5762
5790
  bindings: [
5791
+ linearUserId,
5792
+ ...notDecidedAskBindings,
5763
5793
  ...declaredBindings,
5764
5794
  linearUserId,
5765
5795
  ...DONE_STATE_KEYS,
@@ -5805,36 +5835,57 @@ function resolveUser(sql2, id) {
5805
5835
  }
5806
5836
 
5807
5837
  // node_modules/@catalyst-cloud/read-model/src/issues.ts
5808
- function resolveBlockedOnAsk(sql2, identifiers) {
5838
+ function readAskEdges(sql2, identifiers) {
5809
5839
  const ids = uniqueKeys(identifiers);
5810
- const out = /* @__PURE__ */ new Map();
5811
- if (ids.length === 0) return out;
5840
+ if (ids.length === 0) return [];
5812
5841
  const cond = openAskBlockerConditions("ai");
5842
+ const flagBindings = [ASK_LABEL, ...cond.bindings];
5843
+ const step = MAX_BOUND_PARAMS - flagBindings.length;
5813
5844
  const rows = [];
5814
- const step = MAX_BOUND_PARAMS - cond.bindings.length;
5815
5845
  for (let i = 0; i < ids.length; i += step) {
5816
5846
  const chunk = ids.slice(i, i + step);
5817
5847
  const placeholders2 = chunk.map(() => "?").join(", ");
5818
5848
  rows.push(
5819
5849
  ...sql2.exec(
5820
- `SELECT ar.related_identifier AS held_identifier, ai.identifier AS ask_identifier,
5821
- ai.assignee_id AS ask_assignee_id, ai.assignee AS ask_assignee_legacy,
5822
- ai.created_at AS ask_created_at
5850
+ `SELECT ar.related_identifier AS subject_identifier,
5851
+ ai.identifier AS ask_identifier,
5852
+ ai.title AS ask_title,
5853
+ ai.state AS ask_state,
5854
+ ai.state_type AS ask_state_type,
5855
+ ai.assignee_id AS ask_assignee_id,
5856
+ ai.assignee AS ask_assignee_legacy,
5857
+ ai.created_at AS ask_created_at,
5858
+ ai.updated_at AS ask_updated_at,
5859
+ CASE WHEN ai.title LIKE 'Unblock ' || ar.related_identifier || ' %'
5860
+ THEN 1 ELSE 0 END AS is_subject,
5861
+ EXISTS (SELECT 1 FROM issue_labels ail JOIN labels al ON al.id = ail.label_id
5862
+ WHERE ail.issue_id = ai.id AND al.name = ? AND al.removed_at IS NULL)
5863
+ AS is_covering,
5864
+ CASE WHEN ${cond.sql} THEN 1 ELSE 0 END AS is_open_blocker
5823
5865
  FROM relations ar
5824
5866
  JOIN issues ai ON ai.identifier = ar.issue_identifier AND ai.removed_at IS NULL
5825
- WHERE ar.type IN ('blocks', 'related') AND ar.related_identifier IN (${placeholders2}) AND ${cond.sql}
5867
+ WHERE ar.type IN ('blocks', 'related') AND ar.related_identifier IN (${placeholders2})
5826
5868
  ORDER BY ai.created_at IS NULL, ai.created_at ASC`,
5827
- ...chunk,
5828
- ...cond.bindings
5869
+ ...flagBindings,
5870
+ ...chunk
5829
5871
  ).toArray()
5830
5872
  );
5831
5873
  }
5874
+ return rows;
5875
+ }
5876
+ function resolveBlockedOnAsk(sql2, identifiers) {
5877
+ return blockedOnAskFromEdges(sql2, readAskEdges(sql2, identifiers));
5878
+ }
5879
+ function blockedOnAskFromEdges(sql2, edges) {
5880
+ const out = /* @__PURE__ */ new Map();
5881
+ const rows = edges.filter((r) => r["is_open_blocker"] === 1);
5882
+ if (rows.length === 0) return out;
5832
5883
  const users2 = resolveUsersByIds(
5833
5884
  sql2,
5834
5885
  rows.map((r) => r["ask_assignee_id"])
5835
5886
  );
5836
5887
  for (const r of rows) {
5837
- const held = String(r["held_identifier"]);
5888
+ const held = String(r["subject_identifier"]);
5838
5889
  if (out.has(held)) continue;
5839
5890
  const assigneeId = r["ask_assignee_id"] == null ? null : String(r["ask_assignee_id"]);
5840
5891
  out.set(held, {
@@ -5847,6 +5898,7 @@ function resolveBlockedOnAsk(sql2, identifiers) {
5847
5898
  return out;
5848
5899
  }
5849
5900
  var RELATED_STATE_SUBQUERY = `(SELECT ri.state FROM issues ri WHERE ri.identifier = relations.related_identifier AND ri.removed_at IS NULL LIMIT 1) AS related_state`;
5901
+ var RELATED_TITLE_SUBQUERY = `(SELECT ri.title FROM issues ri WHERE ri.identifier = relations.related_identifier AND ri.removed_at IS NULL LIMIT 1) AS related_title`;
5850
5902
  var MAX_COVERING_ASKS = 5;
5851
5903
  var ANSWERED_STATE_KEYS = ["done", "completed", "released"];
5852
5904
  var CLOSED_UNANSWERED_STATE_KEYS = ["canceled", "cancelled", "duplicate"];
@@ -5985,6 +6037,7 @@ function drivingFacet(scope) {
5985
6037
  if (scope?.labelId?.trim()) return "label";
5986
6038
  if (scope?.initiativeId?.trim()) return "initiative";
5987
6039
  if (scope?.cycleId?.trim()) return "cycle";
6040
+ if (scope?.stateName?.trim()) return "state";
5988
6041
  return null;
5989
6042
  }
5990
6043
  function issueScopeClause(scope) {
@@ -6005,6 +6058,13 @@ function issueScopeClause(scope) {
6005
6058
  facetSql += " AND priority = ?";
6006
6059
  facetBindings.push(scope.priority);
6007
6060
  }
6061
+ const stateName = scope?.stateName?.trim();
6062
+ if (stateName) {
6063
+ const key = `${stateKeySql("issues")} = replace(lower(trim(?)), ' ', '_')`;
6064
+ facetSql += driver === "state" ? ` AND issues.rowid IN (SELECT rowid FROM issues WHERE removed_at IS NULL AND ${key})` : ` AND ${key}`;
6065
+ facetSql += " AND lower(trim(state)) = lower(trim(?))";
6066
+ facetBindings.push(stateName, stateName);
6067
+ }
6008
6068
  const assignee = scope?.assignee;
6009
6069
  const assigneeId = assignee?.linearUserId?.trim();
6010
6070
  const assigneeName = assignee?.name?.trim();
@@ -6117,7 +6177,8 @@ function hydrateIssueRows(sql2, issueRows) {
6117
6177
  const relationRows = execInChunks(
6118
6178
  sql2,
6119
6179
  issueRows.map((r) => r["identifier"]),
6120
- (p) => `SELECT id, type, issue_identifier, related_identifier, ${RELATED_STATE_SUBQUERY}
6180
+ (p) => `SELECT id, type, issue_identifier, related_identifier, ${RELATED_STATE_SUBQUERY},
6181
+ ${RELATED_TITLE_SUBQUERY}
6121
6182
  FROM relations
6122
6183
  WHERE issue_identifier IN (${p})`
6123
6184
  );
@@ -6129,30 +6190,17 @@ function hydrateIssueRows(sql2, issueRows) {
6129
6190
  type: r["type"] ?? null,
6130
6191
  issue_identifier: r["issue_identifier"] ?? null,
6131
6192
  related_identifier: r["related_identifier"] ?? null,
6132
- related_state: r["related_state"] ?? null
6193
+ related_state: r["related_state"] ?? null,
6194
+ related_title: r["related_title"] ?? null
6133
6195
  });
6134
6196
  relationsByIdentifier.set(identifier, list);
6135
6197
  }
6136
- const coveringByIdentifier = /* @__PURE__ */ new Map();
6137
- const coveringRows = execInChunks(
6198
+ const askEdges = readAskEdges(
6138
6199
  sql2,
6139
- issueRows.map((r) => r["identifier"]),
6140
- (p) => `SELECT ar.related_identifier AS subject_identifier,
6141
- ai.identifier AS ask_identifier,
6142
- ai.title AS ask_title,
6143
- ai.state AS ask_state,
6144
- ai.state_type AS ask_state_type,
6145
- ai.assignee_id AS ask_assignee_id,
6146
- ai.updated_at AS ask_updated_at,
6147
- CASE WHEN ai.title LIKE 'Unblock ' || ar.related_identifier || ' %'
6148
- THEN 1 ELSE 0 END AS is_subject
6149
- FROM relations ar
6150
- JOIN issues ai ON ai.identifier = ar.issue_identifier AND ai.removed_at IS NULL
6151
- WHERE ar.type IN ('blocks', 'related') AND ar.related_identifier IN (${p})
6152
- AND EXISTS (SELECT 1 FROM issue_labels ail JOIN labels al ON al.id = ail.label_id
6153
- WHERE ail.issue_id = ai.id AND al.name = ? AND al.removed_at IS NULL)`,
6154
- [ASK_LABEL]
6200
+ issueRows.map((r) => r["identifier"])
6155
6201
  );
6202
+ const coveringByIdentifier = /* @__PURE__ */ new Map();
6203
+ const coveringRows = askEdges.filter((r) => r["is_covering"] === 1);
6156
6204
  for (const r of coveringRows) {
6157
6205
  const subjectIdentifier = String(r["subject_identifier"]);
6158
6206
  const state = r["ask_state"];
@@ -6198,10 +6246,7 @@ function hydrateIssueRows(sql2, issueRows) {
6198
6246
  }))
6199
6247
  );
6200
6248
  }
6201
- const blockedByIdentifier = resolveBlockedOnAsk(
6202
- sql2,
6203
- issueRows.map((r) => r["identifier"])
6204
- );
6249
+ const blockedByIdentifier = blockedOnAskFromEdges(sql2, askEdges);
6205
6250
  return issueRows.map((r) => {
6206
6251
  const id = String(r["id"]);
6207
6252
  const identifier = r["identifier"] ?? null;
@@ -6265,14 +6310,16 @@ function buildIssuesView(sql2, limit = DEFAULT_VIEW_LIMIT, offset = 0, scope) {
6265
6310
  var BLOCKED_COUNT_SQL = blockedOpenCountSql();
6266
6311
  var AGE_UNKNOWN_SENTINEL = 253402300799e3;
6267
6312
  var AGE_KEY_SQL = `COALESCE(created_at, updated_at, ${AGE_UNKNOWN_SENTINEL})`;
6268
- function buildIssueDetail(sql2, identifier) {
6313
+ var NO_ROWS = { exec: () => ({ toArray: () => [] }) };
6314
+ function buildIssueDetail(sql2, identifier, opts = {}) {
6315
+ const sqlFor = (section) => opts.include?.has(section) ?? true ? sql2 : NO_ROWS;
6269
6316
  const issueRows = sql2.exec(
6270
6317
  `SELECT id, identifier, title, description, state, state_id, assignee, assignee_id, priority, estimate,
6271
6318
  project_id, cycle_id, team_id, team_key, team_name,
6272
6319
  delegate_id, delegate_name, bot_actor_name, bot_actor_type, bot_actor_sub_type,
6273
6320
  parent_id, parent_identifier,
6274
6321
  url, started_at, completed_at, canceled_at, created_at, due_date,
6275
- priority_label, sort_order, updated_at
6322
+ priority_label, sort_order, updated_at, ${opts.include == null || opts.include.has("blockers") ? effectiveStateTypeSql("issues") : "NULL"} AS state_type
6276
6323
  FROM issues WHERE identifier = ? AND removed_at IS NULL LIMIT 1`,
6277
6324
  identifier
6278
6325
  ).toArray();
@@ -6286,7 +6333,7 @@ function buildIssueDetail(sql2, identifier) {
6286
6333
  let projectProgress = null;
6287
6334
  let projectHealth = null;
6288
6335
  if (projectId != null) {
6289
- const projectRows = sql2.exec(
6336
+ const projectRows = sqlFor("project").exec(
6290
6337
  "SELECT name, state, description, progress, health FROM projects WHERE id = ? AND removed_at IS NULL LIMIT 1",
6291
6338
  projectId
6292
6339
  ).toArray();
@@ -6302,16 +6349,16 @@ function buildIssueDetail(sql2, identifier) {
6302
6349
  let cycleStartsAt = null;
6303
6350
  let cycleEndsAt = null;
6304
6351
  if (cycleId != null) {
6305
- const cycleRows = sql2.exec("SELECT number, name, starts_at, ends_at FROM cycles WHERE id = ? LIMIT 1", cycleId).toArray();
6352
+ const cycleRows = sqlFor("cycle").exec("SELECT number, name, starts_at, ends_at FROM cycles WHERE id = ? LIMIT 1", cycleId).toArray();
6306
6353
  cycleNumber = cycleRows[0]?.["number"] ?? null;
6307
6354
  cycleName = cycleRows[0]?.["name"] ?? null;
6308
6355
  cycleStartsAt = cycleRows[0]?.["starts_at"] ?? null;
6309
6356
  cycleEndsAt = cycleRows[0]?.["ends_at"] ?? null;
6310
6357
  }
6311
- const assigneeUser = resolveUser(sql2, r["assignee_id"] ?? null);
6312
- const delegateUser = resolveUser(sql2, r["delegate_id"] ?? null);
6313
- const activity = buildIssueActivity(sql2, id);
6314
- const comments2 = sql2.exec(
6358
+ const assigneeUser = resolveUser(sqlFor("users"), r["assignee_id"] ?? null);
6359
+ const delegateUser = resolveUser(sqlFor("users"), r["delegate_id"] ?? null);
6360
+ const activity = buildIssueActivity(sqlFor("activity"), id);
6361
+ const comments2 = sqlFor("comments").exec(
6315
6362
  "SELECT id, body, author_id, author_name, author_avatar_url, is_bot, parent_id, created_at, updated_at FROM comments WHERE issue_id = ? AND removed_at IS NULL ORDER BY updated_at",
6316
6363
  id
6317
6364
  ).toArray().map((c) => ({
@@ -6325,7 +6372,7 @@ function buildIssueDetail(sql2, identifier) {
6325
6372
  created_at: c["created_at"] ?? null,
6326
6373
  updated_at: c["updated_at"] ?? null
6327
6374
  }));
6328
- const agent_sessions2 = sql2.exec(
6375
+ const agent_sessions2 = sqlFor("agentSessions").exec(
6329
6376
  `SELECT id, status, app_user_id, creator_id, created_at, updated_at
6330
6377
  FROM agent_sessions WHERE issue_id = ? AND removed_at IS NULL
6331
6378
  ORDER BY updated_at DESC`,
@@ -6357,7 +6404,7 @@ function buildIssueDetail(sql2, identifier) {
6357
6404
  }))
6358
6405
  };
6359
6406
  });
6360
- const labels2 = sql2.exec(
6407
+ const labels2 = sqlFor("labels").exec(
6361
6408
  `SELECT l.id AS id, l.name AS name, l.color AS color
6362
6409
  FROM issue_labels il JOIN labels l ON l.id = il.label_id
6363
6410
  WHERE il.issue_id = ? AND l.removed_at IS NULL`,
@@ -6367,17 +6414,18 @@ function buildIssueDetail(sql2, identifier) {
6367
6414
  name: l["name"] ?? null,
6368
6415
  color: l["color"] ?? null
6369
6416
  }));
6370
- const relations2 = sql2.exec(
6371
- `SELECT id, type, issue_identifier, related_identifier, ${RELATED_STATE_SUBQUERY} FROM relations WHERE issue_identifier = ?`,
6417
+ const relations2 = sqlFor("relations").exec(
6418
+ `SELECT id, type, issue_identifier, related_identifier, ${RELATED_STATE_SUBQUERY}, ${RELATED_TITLE_SUBQUERY} FROM relations WHERE issue_identifier = ?`,
6372
6419
  identifier
6373
6420
  ).toArray().map((rel) => ({
6374
6421
  id: rel["id"] ?? null,
6375
6422
  type: rel["type"] ?? null,
6376
6423
  issue_identifier: rel["issue_identifier"] ?? null,
6377
6424
  related_identifier: rel["related_identifier"] ?? null,
6378
- related_state: rel["related_state"] ?? null
6425
+ related_state: rel["related_state"] ?? null,
6426
+ related_title: rel["related_title"] ?? null
6379
6427
  }));
6380
- const linkedPulls = sql2.exec(
6428
+ const linkedPulls = sqlFor("linkedPulls").exec(
6381
6429
  `SELECT repo_id, number, node_id, title, state, draft, merged
6382
6430
  FROM pull_requests WHERE linear_issue_identifier = ? ORDER BY updated_at DESC`,
6383
6431
  identifier
@@ -6390,6 +6438,7 @@ function buildIssueDetail(sql2, identifier) {
6390
6438
  draft: p["draft"] ?? null,
6391
6439
  merged: p["merged"] ?? null
6392
6440
  }));
6441
+ const blockedBy = readOpenBlockers(sqlFor("blockers"), identifier);
6393
6442
  return {
6394
6443
  id,
6395
6444
  identifier: r["identifier"] ?? null,
@@ -6397,6 +6446,7 @@ function buildIssueDetail(sql2, identifier) {
6397
6446
  description: r["description"] ?? null,
6398
6447
  state: r["state"] ?? null,
6399
6448
  state_id: r["state_id"] ?? null,
6449
+ state_type: r["state_type"] ?? null,
6400
6450
  assignee: r["assignee"] ?? null,
6401
6451
  assignee_id: r["assignee_id"] ?? null,
6402
6452
  assignee_name: assigneeUser.name ?? r["assignee"] ?? null,
@@ -6439,9 +6489,83 @@ function buildIssueDetail(sql2, identifier) {
6439
6489
  activity,
6440
6490
  labels: labels2,
6441
6491
  relations: relations2,
6442
- linked_pulls: linkedPulls
6492
+ linked_pulls: linkedPulls,
6493
+ blocked_by: blockedBy
6443
6494
  };
6444
6495
  }
6496
+ var DONE_KEY_LIST = DONE_STATE_KEYS.map(() => "?").join(", ");
6497
+ function readOpenBlockers(sql2, identifier) {
6498
+ const rows = sql2.exec(
6499
+ `SELECT DISTINCT br.issue_identifier AS identifier, bi.id AS mirrored_id,
6500
+ (SELECT COUNT(*) FROM issues lx
6501
+ WHERE lx.identifier = br.issue_identifier AND lx.removed_at IS NULL) AS live_rows,
6502
+ bi.title AS title, bi.state AS state,
6503
+ CASE WHEN EXISTS (SELECT 1 FROM issue_labels kil JOIN labels kl ON kl.id = kil.label_id
6504
+ WHERE kil.issue_id = bi.id AND kl.removed_at IS NULL
6505
+ AND (kl.name = ? OR substr(kl.name, 1, 4) = ?))
6506
+ THEN 1 ELSE 0 END AS is_ask,
6507
+ CASE WHEN EXISTS (SELECT 1 FROM issue_labels ail JOIN labels al ON al.id = ail.label_id
6508
+ WHERE ail.issue_id = bi.id AND al.name = ? AND al.removed_at IS NULL)
6509
+ THEN 1 ELSE 0 END AS is_approval,
6510
+ CASE WHEN EXISTS (SELECT 1 FROM comments dc
6511
+ WHERE dc.issue_id = bi.id AND dc.removed_at IS NULL
6512
+ AND dc.body LIKE 'DECIDED:%')
6513
+ THEN 1 ELSE 0 END AS is_decided,
6514
+ bi.assignee_id AS assignee_id, bi.assignee AS assignee, bi.created_at AS created_at
6515
+ FROM relations br
6516
+ LEFT JOIN issues bi ON bi.identifier = br.issue_identifier AND bi.removed_at IS NULL
6517
+ WHERE br.type = 'blocks' AND br.related_identifier = ? AND br.issue_identifier != ''
6518
+ AND (bi.id IS NULL OR live_rows != 1 OR ${BLOCKER_OPEN_SQL})
6519
+ ORDER BY bi.id IS NULL, bi.created_at IS NULL, bi.created_at ASC, br.issue_identifier ASC`,
6520
+ ASK_LABEL,
6521
+ ASK_LABEL_PREFIX,
6522
+ APPROVAL_ASK_LABEL,
6523
+ identifier,
6524
+ ...TERMINAL_WORKFLOW_STATE_TYPES,
6525
+ ...DONE_STATE_KEYS
6526
+ ).toArray();
6527
+ const users2 = resolveUsersByIds(
6528
+ sql2,
6529
+ rows.map((b) => b["assignee_id"] ?? null)
6530
+ );
6531
+ const seen = /* @__PURE__ */ new Set();
6532
+ const out = [];
6533
+ for (const b of rows) {
6534
+ const id = String(b["identifier"]);
6535
+ if (seen.has(id)) continue;
6536
+ seen.add(id);
6537
+ if (Number(b["live_rows"]) !== 1) {
6538
+ out.push({
6539
+ identifier: id,
6540
+ title: null,
6541
+ state: null,
6542
+ is_ask: false,
6543
+ is_approval: false,
6544
+ is_decided: false,
6545
+ unresolved: true,
6546
+ assignee_id: null,
6547
+ assignee_name: null
6548
+ });
6549
+ continue;
6550
+ }
6551
+ const assigneeId = b["assignee_id"] ?? null;
6552
+ out.push({
6553
+ identifier: b["identifier"] ?? null,
6554
+ title: b["title"] ?? null,
6555
+ state: b["state"] ?? null,
6556
+ is_ask: Number(b["is_ask"]) === 1,
6557
+ is_approval: Number(b["is_approval"]) === 1,
6558
+ is_decided: Number(b["is_decided"]) === 1,
6559
+ unresolved: false,
6560
+ assignee_id: assigneeId,
6561
+ assignee_name: (assigneeId == null ? void 0 : users2.get(String(assigneeId))?.name) ?? b["assignee"] ?? null
6562
+ });
6563
+ }
6564
+ return out;
6565
+ }
6566
+ var TERMINAL_WORKFLOW_STATE_TYPES = ["completed", "canceled", "duplicate"];
6567
+ var BLOCKER_OPEN_SQL = `(CASE WHEN ${effectiveStateTypeSql("bi")} IS NOT NULL THEN ${effectiveStateTypeSql("bi")} NOT IN (${TERMINAL_WORKFLOW_STATE_TYPES.map(() => "?").join(", ")}) ELSE COALESCE(${stateKeySql("bi")}, '') NOT IN (${DONE_KEY_LIST}) END)`;
6568
+ var APPROVAL_ASK_LABEL = "ask/approval";
6445
6569
  var REVIEW_FACET_STATE = {
6446
6570
  approved: "APPROVED",
6447
6571
  changes: "CHANGES_REQUESTED",
@@ -6464,6 +6588,10 @@ function blockedOnAskSql() {
6464
6588
  function pullScopeParts(scope) {
6465
6589
  const parts = [];
6466
6590
  const bindings = [];
6591
+ if (scope?.repositoryIds !== void 0) {
6592
+ parts.push("lower(repo_id) IN (SELECT value FROM json_each(?))");
6593
+ bindings.push(JSON.stringify([...new Set(scope.repositoryIds.map((id) => id.toLowerCase()))]));
6594
+ }
6467
6595
  const repoId = scope?.repoId?.trim();
6468
6596
  if (repoId) {
6469
6597
  parts.push("lower(repo_id) = ?");
@@ -6500,19 +6628,43 @@ function pullScopeClause(scope) {
6500
6628
  const { parts, bindings } = pullScopeParts(scope);
6501
6629
  return { sql: parts.length > 0 ? ` WHERE ${parts.join(" AND ")}` : "", bindings };
6502
6630
  }
6503
- function hydratePullRows(sql2, prRows) {
6631
+ function hydratePullRows(sql2, prRows, projection) {
6504
6632
  if (prRows.length === 0) return [];
6505
6633
  const checksByRepoSha = /* @__PURE__ */ new Map();
6506
- const checkRows = execInChunks(
6634
+ const summariesByRepoSha = /* @__PURE__ */ new Map();
6635
+ const newestRuns = (p) => `SELECT repo_id, head_sha, name, status, conclusion FROM (
6636
+ SELECT repo_id, head_sha, name, status, conclusion,
6637
+ ROW_NUMBER() OVER (PARTITION BY repo_id, head_sha, name
6638
+ ORDER BY CAST(check_run_id AS INTEGER) DESC, check_run_id DESC) AS newest
6639
+ FROM check_runs WHERE head_sha IN (${p}))
6640
+ WHERE newest = 1`;
6641
+ const checkRows = projection === "summary" ? execInChunks(
6642
+ sql2,
6643
+ prRows.map((r) => r["head_sha"]),
6644
+ (p) => `SELECT repo_id, head_sha,
6645
+ SUM(CASE WHEN status = 'completed' AND conclusion = 'success' THEN 1 ELSE 0 END) AS passed,
6646
+ SUM(CASE WHEN status = 'completed' AND conclusion IN ('failure', 'timed_out', 'action_required', 'startup_failure') THEN 1 ELSE 0 END) AS failed,
6647
+ SUM(CASE WHEN status IS NULL OR status <> 'completed' THEN 1 ELSE 0 END) AS pending,
6648
+ SUM(CASE WHEN status = 'completed' AND (conclusion IS NULL OR conclusion NOT IN ('success', 'failure', 'timed_out', 'action_required', 'startup_failure')) THEN 1 ELSE 0 END) AS neutral
6649
+ FROM (${newestRuns(p)}) GROUP BY repo_id, head_sha`
6650
+ ) : execInChunks(
6507
6651
  sql2,
6508
6652
  prRows.map((r) => r["head_sha"]),
6509
- (p) => `SELECT repo_id, head_sha, name, status, conclusion FROM check_runs
6510
- WHERE head_sha IN (${p})`
6653
+ newestRuns
6511
6654
  );
6512
6655
  for (const r of checkRows) {
6513
6656
  const sha = r["head_sha"] == null ? "" : String(r["head_sha"]);
6514
6657
  if (sha === "") continue;
6515
6658
  const key = `${r["repo_id"] == null ? "" : String(r["repo_id"])}\0${sha}`;
6659
+ if (projection === "summary") {
6660
+ summariesByRepoSha.set(key, {
6661
+ passed: Number(r["passed"] ?? 0),
6662
+ failed: Number(r["failed"] ?? 0),
6663
+ pending: Number(r["pending"] ?? 0),
6664
+ neutral: Number(r["neutral"] ?? 0)
6665
+ });
6666
+ continue;
6667
+ }
6516
6668
  const list = checksByRepoSha.get(key) ?? [];
6517
6669
  list.push({
6518
6670
  name: r["name"] ?? null,
@@ -6521,7 +6673,6 @@ function hydratePullRows(sql2, prRows) {
6521
6673
  });
6522
6674
  checksByRepoSha.set(key, list);
6523
6675
  }
6524
- const remediateReviewKeys = /* @__PURE__ */ new Set();
6525
6676
  const latestReviewByReviewer = /* @__PURE__ */ new Map();
6526
6677
  const numbersByRepo = /* @__PURE__ */ new Map();
6527
6678
  for (const r of prRows) {
@@ -6559,10 +6710,15 @@ function hydratePullRows(sql2, prRows) {
6559
6710
  latestReviewByReviewer.set(reviewerKey, { state, submittedAt, reviewId, repoId, prNumber });
6560
6711
  }
6561
6712
  }
6713
+ const summariesByPr = /* @__PURE__ */ new Map();
6562
6714
  for (const { state, repoId, prNumber } of latestReviewByReviewer.values()) {
6563
- if (state === "COMMENTED" || state === "CHANGES_REQUESTED") {
6564
- remediateReviewKeys.add(`${repoId}#${prNumber}`);
6565
- }
6715
+ const k = `${repoId}#${prNumber}`;
6716
+ const summary = summariesByPr.get(k) ?? { reviewers: 0, approved: 0, changes: 0, commented: 0 };
6717
+ summary.reviewers += 1;
6718
+ if (state === "APPROVED") summary.approved += 1;
6719
+ else if (state === "CHANGES_REQUESTED") summary.changes += 1;
6720
+ else if (state === "COMMENTED") summary.commented += 1;
6721
+ summariesByPr.set(k, summary);
6566
6722
  }
6567
6723
  const failingStatusKeys = /* @__PURE__ */ new Set();
6568
6724
  const statusRows = execInChunks(
@@ -6586,6 +6742,15 @@ function hydratePullRows(sql2, prRows) {
6586
6742
  const headSha = r["head_sha"] == null ? "" : String(r["head_sha"]);
6587
6743
  const checkKey = `${r["repo_id"] == null ? "" : String(r["repo_id"])}\0${headSha}`;
6588
6744
  const linkedIdentifier = r["linear_issue_identifier"];
6745
+ const reviewSummary = (() => {
6746
+ const s = summariesByPr.get(`${r["repo_id"] ?? ""}#${r["number"] ?? ""}`);
6747
+ return s === void 0 ? { reviewers: 0, approved: 0, changes: 0, commented: 0 } : {
6748
+ reviewers: s.reviewers,
6749
+ approved: s.approved,
6750
+ changes: s.changes,
6751
+ commented: s.commented
6752
+ };
6753
+ })();
6589
6754
  return {
6590
6755
  repo_id: r["repo_id"] ?? null,
6591
6756
  number: r["number"] ?? null,
@@ -6611,7 +6776,16 @@ function hydratePullRows(sql2, prRows) {
6611
6776
  head_ref: r["head_ref"] ?? null,
6612
6777
  linear_issue_identifier: r["linear_issue_identifier"] ?? null,
6613
6778
  checks: headSha === "" ? [] : checksByRepoSha.get(checkKey) ?? [],
6614
- has_review_signal: remediateReviewKeys.has(`${r["repo_id"] ?? ""}#${r["number"] ?? ""}`) ? 1 : 0,
6779
+ ...projection === "summary" ? {
6780
+ checks_summary: summariesByRepoSha.get(checkKey) ?? {
6781
+ passed: 0,
6782
+ failed: 0,
6783
+ pending: 0,
6784
+ neutral: 0
6785
+ }
6786
+ } : {},
6787
+ review_summary: reviewSummary,
6788
+ has_review_signal: reviewSummary.changes + reviewSummary.commented > 0 ? 1 : 0,
6615
6789
  has_failing_status: headSha !== "" && failingStatusKeys.has(checkKey) ? 1 : 0,
6616
6790
  blocked_on_ask: linkedIdentifier == null ? null : blockedByIdentifier.get(String(linkedIdentifier)) ?? null
6617
6791
  };
@@ -7140,6 +7314,41 @@ var READ_INDEXES = [
7140
7314
  name: "idx_check_suites_branch",
7141
7315
  table: "check_suites",
7142
7316
  sql: "CREATE INDEX IF NOT EXISTS idx_check_suites_branch ON check_suites(head_branch, updated_at)"
7317
+ },
7318
+ {
7319
+ // CTC-1457 — the check-run ingest's PR resolve (apps/mirror/src/do/MirrorDO.ts handleIngest,
7320
+ // `SELECT number FROM pull_requests WHERE repo_id = ? AND head_sha = ?`). The primary key
7321
+ // `(repo_id, number)` seeks the repository and then walks every one of its PRs: tenant-0 on
7322
+ // 2026-09-28 ran it 36,551 times at 3,709 rows each (about 135M rows a day). With this entry the
7323
+ // lookup seeks the sha and the repository. `repo_id` is in the key because forks share commit
7324
+ // shas: a sha-only key would read every fork's PR at that sha before filtering (Codex review). Rows written: 1 more per pull request upsert (about 5,300 to 7,000 a day on
7325
+ // tenant-0). Both measured in apps/mirror/test-integration/check-run-pull-rows-read.test.ts; plan
7326
+ // and drop-control in apps/mirror/test/do-rows-read-query-plan.test.ts.
7327
+ name: "idx_pulls_head_sha_repo",
7328
+ table: "pull_requests",
7329
+ sql: "CREATE INDEX IF NOT EXISTS idx_pulls_head_sha_repo ON pull_requests(head_sha, repo_id)"
7330
+ },
7331
+ // CTC-4505 — the Waiting view (waitingOnMeClause): a member's OPEN issues. Measured on tenant-0
7332
+ // from durable_object_storage_exec spans, 2026-10-01 22:30-23:10Z: the page and its count each
7333
+ // walked every live issue (18.9k and 18.8k rows), because the member and open-state tests sat inside
7334
+ // an OR. The clause now repeats this WHERE as a top-level conjunct, built from the same constant, so
7335
+ // SQLite seeks here: 201k to 27k VM steps per statement in a replay on a copy of a tenant-0 replica.
7336
+ // ⚠️ Not CTC-3050's rejected `issues(assignee_id, ...)`: that indexed every issue, and one member
7337
+ // holds 60% of them; this holds open issues only (about 2.3k of 8.6k).
7338
+ {
7339
+ // The name carries a hash of the WHERE, so a changed done-state list builds a new index.
7340
+ name: OPEN_ASSIGNEE_INDEX_NAME,
7341
+ table: "issues",
7342
+ sql: `CREATE INDEX IF NOT EXISTS ${OPEN_ASSIGNEE_INDEX_NAME} ON issues(assignee_id) WHERE ${OPEN_ASSIGNEE_INDEX_WHERE}`
7343
+ },
7344
+ // CTC-4505: answered-receipt probes in readAskNudges. Match the reader's literal predicate;
7345
+ // unconstrained nudge/operator receipt lists cannot use this partial index, preserving their
7346
+ // timestamp-tie ordering. Workerd counters and full-answer controls live in the mirror test.
7347
+ // The hub creates this table before ensureReadModelDdl; replicas skip its absent table.
7348
+ {
7349
+ name: "idx_ask_tap_receipts_answered_member_issue",
7350
+ table: "ask_tap_receipts",
7351
+ sql: "CREATE INDEX IF NOT EXISTS idx_ask_tap_receipts_answered_member_issue ON ask_tap_receipts(member_id, issue_id) WHERE outcome IN ('resolved', 'already-resolved')"
7143
7352
  }
7144
7353
  ];
7145
7354
  var RETIRED_READ_INDEXES = [
@@ -73,6 +73,9 @@ export interface IssueRelationView {
73
73
  * freeze the value forever. The exact bug CTC-885 reports would survive its own fix, invisibly.
74
74
  */
75
75
  related_state: SqlValue;
76
+ /** CTC-4183 — the far end's title, resolved at read time like `related_state`; NULL when the target
77
+ * is not mirrored (or is soft-deleted). Optional on the type for rows from a reader that predates it. */
78
+ related_title?: SqlValue;
76
79
  }
77
80
  /** One issue row for the list view — snake_case mirror columns + denormalized labels/relations. */
78
81
  export interface IssueView {
@@ -353,6 +356,9 @@ export interface IssueDetailView {
353
356
  state: SqlValue;
354
357
  /** The workflow state's stable Linear id (CTC-148) — behind the `state` name. */
355
358
  state_id: SqlValue;
359
+ /** CTC-3326 — the EFFECTIVE workflow-state type ({@link effectiveStateTypeSql}), as the list row
360
+ * carries it, so the ticket page decides "finished" the way dispatch does. */
361
+ state_type: SqlValue;
356
362
  assignee: SqlValue;
357
363
  /** Assignee identity (CTC-USERS): the FK + resolved name/avatar (null when the user isn't mirrored —
358
364
  * never fabricated). `assignee_name` falls back to the legacy `assignee` name string. */
@@ -414,6 +420,42 @@ export interface IssueDetailView {
414
420
  /** The PRs that reference this ticket (CTC-269) — the ticket→PR side of the cross-link. Reverse
415
421
  * lookup on `pull_requests.linear_issue_identifier`; empty [] when no mirrored PR names this key. */
416
422
  linked_pulls: IssueLinkedPullView[];
423
+ /** CTC-3326 — the OPEN tickets holding this one up, from their own outgoing `blocks` edges. See
424
+ * {@link IssueBlockerView}. Empty [] when nothing open blocks it. */
425
+ blocked_by: IssueBlockerView[];
426
+ }
427
+ /**
428
+ * CTC-3326 — one open ticket that blocks the detail's issue: the far end of an INBOUND `blocks` edge.
429
+ *
430
+ * The mirror stores no inverse row (Linear fires no relation webhooks), so an inbound edge is the
431
+ * blocker's own outgoing row, `relations.related_identifier = <this issue>`, the same read the unblock
432
+ * sweep and `/inspect` make. `is_ask` is the ask label family, with the same constants and the same
433
+ * exact `substr` match as {@link openAskBlockerConditions}, but WITHOUT that helper's name-based
434
+ * "not done" test: openness is already decided by workflow type, and an ask in a started state named
435
+ * "Released" is still an ask (Codex review, PR #6342).
436
+ */
437
+ export interface IssueBlockerView {
438
+ identifier: SqlValue;
439
+ title: SqlValue;
440
+ state: SqlValue;
441
+ /** Whether the blocker carries the ask label family (`catalyst-ask` or `ask/*`). */
442
+ is_ask: boolean;
443
+ /** Whether it is an `ask/approval`, which only PORTAL redeems: never answered in the app. */
444
+ is_approval: boolean;
445
+ /** Whether the ask already carries its server-written `DECIDED:` comment: the same signal the
446
+ * mirror's `alreadyResolved` reads, and one the replica holds too. The hold-ask table the
447
+ * eligibility check reads (`relay_hold_ask`) lives only in the DO, so it cannot decide this here.
448
+ * A decided ask is never asked again; a `hand-fix` answer can still hold the ticket, so the
449
+ * reader decides how to show it rather than this read dropping it. */
450
+ is_decided: boolean;
451
+ /** CTC-3326 (Codex review, PR #6342) — the edge names a blocker with no live mirrored `issues` row
452
+ * (a cross-team or unfetched dependency, or a removed one). Dispatch holds on it as an unknown
453
+ * blocker, so it is listed rather than dropped; its title and state are null. */
454
+ unresolved: boolean;
455
+ /** Who answers it. An ask is answered only by its assignee (`answerAskWithOption`'s gate). */
456
+ assignee_id: SqlValue;
457
+ /** The assignee's resolved name, falling back to the legacy `assignee` string; null when none. */
458
+ assignee_name: SqlValue;
417
459
  }
418
460
  /** A PR that references an issue (CTC-269) — the ticket→PR reverse-lookup row. Carries just enough to
419
461
  * render the Relations card row (a state dot + title) and deep-link straight to the PR on GitHub
@@ -435,6 +477,18 @@ export interface PullCheckView {
435
477
  status: SqlValue;
436
478
  conclusion: SqlValue;
437
479
  }
480
+ /** A check on a PR detail: the pill plus GitHub's check-run id. A re-run is a second row with the same
481
+ * name, and the SELECT is unordered, so the client picks the newest attempt by this id, which GitHub
482
+ * assigns in creation order (`latestCheckRunByName`). Stored as text; null only on a partial row. */
483
+ export interface PullDetailCheckView extends PullCheckView {
484
+ check_run_id: SqlValue;
485
+ }
486
+ export interface PullChecksSummary {
487
+ passed: number;
488
+ failed: number;
489
+ pending: number;
490
+ neutral: number;
491
+ }
438
492
  /** One PR row for the list view — snake_case mirror columns + denormalized checks. */
439
493
  export interface PullView {
440
494
  repo_id: SqlValue;
@@ -466,8 +520,14 @@ export interface PullView {
466
520
  head_ref: SqlValue;
467
521
  linear_issue_identifier: SqlValue;
468
522
  checks: PullCheckView[];
523
+ /** Present only for the additive cloud summary projection; raw checks stay on detail reads. */
524
+ checks_summary?: PullChecksSummary;
525
+ /** CTC-3110 — each reviewer's latest verdict on this PR, counted ({@link PullReviewSummary}), so a
526
+ * client holding every row answers the review facet exactly ({@link pullMatchesReviewFacet}). */
527
+ review_summary: PullReviewSummary;
469
528
  /** CTC-312: 1 when the PR has any review with state COMMENTED or CHANGES_REQUESTED
470
- * (case-insensitive). The board's Remediate column reads this; the list view ignores it. */
529
+ * (case-insensitive) — exactly `review_summary.changes + review_summary.commented > 0`. The
530
+ * board's Remediate column reads this; the list view ignores it. */
471
531
  has_review_signal: number;
472
532
  /** CTC-312: 1 when the PR's head sha carries a legacy commit STATUS in a failing state
473
533
  * (failure/error). The board's Remediate signal reads this so a repo that reports CI via the
@@ -532,13 +592,23 @@ export interface PullDetailView {
532
592
  head_ref: SqlValue;
533
593
  linear_issue_identifier: SqlValue;
534
594
  linked_issue_state: SqlValue;
535
- checks: PullCheckView[];
595
+ checks: PullDetailCheckView[];
536
596
  reviews: PullReviewView[];
537
597
  commit_statuses: PullStatusView[];
538
598
  /** CTC-1912 — same as `PullView.blocked_on_ask`. Not existence-gated on the linked issue's row
539
599
  * (unlike `linear_issue_identifier`/`linked_issue_state`): the edge itself is the evidence. */
540
600
  blocked_on_ask: BlockedOnAskView | null;
541
601
  }
602
+ /** CTC-3110 — each reviewer's LATEST review ({@link isLaterReview}) on one PR, counted by verdict. The
603
+ * list row carries it so a client holding every row (the browser replica) answers the review facet
604
+ * exactly as {@link pullScopeParts} does in SQL. A reviewer is a distinct `user_id` (null is one). */
605
+ export interface PullReviewSummary {
606
+ /** Reviewers with at least one review row. 0 is the `none` facet. */
607
+ reviewers: number;
608
+ approved: number;
609
+ changes: number;
610
+ commented: number;
611
+ }
542
612
  /** A denormalized parent-initiative reference on a project row (via project_initiatives).
543
613
  * `id` is always present (the edge exists); `name` resolves from the initiatives table and is
544
614
  * null when that initiative isn't mirrored — never fabricated. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catalyst-cloud/sdk",
3
- "version": "0.14.2",
3
+ "version": "0.16.0",
4
4
  "description": "Read and update your Catalyst Cloud data, and receive live changes in your apps.",
5
5
  "license": "MIT",
6
6
  "author": "Coalesce Labs",
@@ -86,7 +86,8 @@
86
86
  "test": "vitest run",
87
87
  "prepublishOnly": "npm run build",
88
88
  "typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
89
- "test:pack": "node scripts/verify-pack.mjs"
89
+ "test:pack": "node scripts/verify-pack.mjs",
90
+ "release:check": "node scripts/release-train-check.mjs && node --test scripts/release-train-check.test.mjs"
90
91
  },
91
92
  "keywords": [
92
93
  "catalyst",
@@ -98,9 +99,9 @@
98
99
  "durable-objects"
99
100
  ],
100
101
  "devDependencies": {
101
- "@catalyst-cloud/read-model": "^0.13.0",
102
- "@catalyst-cloud/replicate": "0.13.2",
103
- "@catalyst-cloud/schema": "0.13.2",
102
+ "@catalyst-cloud/read-model": "^0.16.0",
103
+ "@catalyst-cloud/replicate": "0.16.0",
104
+ "@catalyst-cloud/schema": "0.16.0",
104
105
  "@opentelemetry/api": "^1.9.0",
105
106
  "@opentelemetry/context-async-hooks": "^2.8.0",
106
107
  "@opentelemetry/resources": "^2.8.0",
@@ -117,9 +118,9 @@
117
118
  "@opentelemetry/api": ">=1.3.0 <2",
118
119
  "@sqlite.org/sqlite-wasm": "*",
119
120
  "better-sqlite3": "*",
120
- "@catalyst-cloud/read-model": "^0.13.0",
121
- "@catalyst-cloud/replicate": "0.13.2",
122
- "@catalyst-cloud/schema": "0.13.2"
121
+ "@catalyst-cloud/read-model": "^0.16.0",
122
+ "@catalyst-cloud/replicate": "0.16.0",
123
+ "@catalyst-cloud/schema": "0.16.0"
123
124
  },
124
125
  "peerDependenciesMeta": {
125
126
  "@opentelemetry/api": {