@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 +271 -62
- package/dist/tenant-client.d.ts +72 -2
- package/package.json +9 -8
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
|
-
|
|
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:
|
|
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
|
|
5838
|
+
function readAskEdges(sql2, identifiers) {
|
|
5809
5839
|
const ids = uniqueKeys(identifiers);
|
|
5810
|
-
|
|
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
|
|
5821
|
-
ai.
|
|
5822
|
-
ai.
|
|
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})
|
|
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
|
-
...
|
|
5828
|
-
...
|
|
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["
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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(
|
|
6312
|
-
const delegateUser = resolveUser(
|
|
6313
|
-
const activity = buildIssueActivity(
|
|
6314
|
-
const comments2 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
6564
|
-
|
|
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
|
-
|
|
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 = [
|
package/dist/tenant-client.d.ts
CHANGED
|
@@ -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)
|
|
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:
|
|
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.
|
|
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.
|
|
102
|
-
"@catalyst-cloud/replicate": "0.
|
|
103
|
-
"@catalyst-cloud/schema": "0.
|
|
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.
|
|
121
|
-
"@catalyst-cloud/replicate": "0.
|
|
122
|
-
"@catalyst-cloud/schema": "0.
|
|
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": {
|