@catalyst-cloud/schema 0.1.53 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/events/registry.ts +28 -0
- package/src/migrations.generated.ts +18 -0
- package/src/mirror.ts +12 -3
- package/src/telemetry/phase-lifecycle-point.ts +16 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catalyst-cloud/schema",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.55",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Typed Drizzle schema = single source of truth for the per-tenant Mirror DO SQLite store (CTC-13 / ADR-0002). Shared by the mirror Worker, the host-sync replica, and the browser OPFS replica.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@catalyst-cloud/typescript-config": "workspace:*",
|
|
34
|
-
"@types/node": "^26.
|
|
34
|
+
"@types/node": "^26.5.1",
|
|
35
35
|
"drizzle-kit": "^0.31.0",
|
|
36
36
|
"prettier": "^3.9.6",
|
|
37
37
|
"typescript": "^7.0.2",
|
package/src/events/registry.ts
CHANGED
|
@@ -1288,6 +1288,20 @@ export const durableEventTypes = {
|
|
|
1288
1288
|
wakes: false,
|
|
1289
1289
|
entity: "ticket",
|
|
1290
1290
|
},
|
|
1291
|
+
"phase.remediate.decision-raised": {
|
|
1292
|
+
rationale:
|
|
1293
|
+
"ADR-0039 durable-fact example: the mirror actually raised (or attached to an existing) a catalyst-ask decision ticket for a decision an accepted remediate decision-surfaced record named (CTC-1737) — a confirmed, DO-side fact, distinct from `phase.remediate.decision-surfaced`, which merely reported the session's own words.",
|
|
1294
|
+
ingestVerb: "coordination.publish",
|
|
1295
|
+
wakes: false,
|
|
1296
|
+
entity: "ticket",
|
|
1297
|
+
},
|
|
1298
|
+
"phase.remediate.decision-surfaced": {
|
|
1299
|
+
rationale:
|
|
1300
|
+
"ADR-0039 durable-fact example: a remediate round (ANY tier, unlike `phase.remediate.escalation-exhausted`) reported a genuine human decision via its own decisions.json (CTC-1737) — the requesting signal that asks the mirror to raise a blocking catalyst-ask ticket; see the paired `phase.remediate.decision-raised` once one is confirmed.",
|
|
1301
|
+
ingestVerb: "coordination.publish",
|
|
1302
|
+
wakes: false,
|
|
1303
|
+
entity: "ticket",
|
|
1304
|
+
},
|
|
1291
1305
|
"phase.remediate.escalated": {
|
|
1292
1306
|
rationale:
|
|
1293
1307
|
"ADR-0039 durable-fact example: a remediate round's cap-exhaustion escalated to a second, broader-purview attempt instead of parking (CTC-1373) — a distinct lifecycle transition from an ordinary `phase.remediate.failed`, and one that creates newly claimable work.",
|
|
@@ -1678,6 +1692,20 @@ export const durableEventTypes = {
|
|
|
1678
1692
|
wakes: false,
|
|
1679
1693
|
entity: "ticket",
|
|
1680
1694
|
},
|
|
1695
|
+
"phase.validate.decision-raised": {
|
|
1696
|
+
rationale:
|
|
1697
|
+
"ADR-0039 durable-fact example: the mirror actually raised (or attached to an existing) a catalyst-ask decision ticket for a decision an accepted validate decision-surfaced record named (CTC-1737) — a confirmed, DO-side fact, distinct from `phase.validate.decision-surfaced`, which merely reported the session's own words.",
|
|
1698
|
+
ingestVerb: "coordination.publish",
|
|
1699
|
+
wakes: false,
|
|
1700
|
+
entity: "ticket",
|
|
1701
|
+
},
|
|
1702
|
+
"phase.validate.decision-surfaced": {
|
|
1703
|
+
rationale:
|
|
1704
|
+
"ADR-0039 durable-fact example: a validate round reported a genuine human decision via its own decisions.json (CTC-1737) — `validate` had no decision vocabulary at all before this; the requesting signal that asks the mirror to raise a blocking catalyst-ask ticket, paired with `phase.validate.decision-raised` once one is confirmed.",
|
|
1705
|
+
ingestVerb: "coordination.publish",
|
|
1706
|
+
wakes: false,
|
|
1707
|
+
entity: "ticket",
|
|
1708
|
+
},
|
|
1681
1709
|
"phase.validate.failed": {
|
|
1682
1710
|
rationale:
|
|
1683
1711
|
"ADR-0039 durable-fact example ('a phase started, completed, failed, or was preempted'): the phase reported failure. CTC-1541 — absent until now, so the record the runner has always published was dropped at deriveDurableCoordinationEvents's classification filter and never reached phase-failure-trigger.ts; ADR-0069 D3's 'every per-ticket failure drives the trigger' was not true for validate.",
|
|
@@ -291,6 +291,20 @@ export const MIRROR_MIGRATIONS = {
|
|
|
291
291
|
tag: "0039_unknown_nebula",
|
|
292
292
|
breakpoints: true,
|
|
293
293
|
},
|
|
294
|
+
{
|
|
295
|
+
idx: 40,
|
|
296
|
+
version: "6",
|
|
297
|
+
when: 1789188558404,
|
|
298
|
+
tag: "0040_ctc1885_workflow_state_name_backfill",
|
|
299
|
+
breakpoints: true,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
idx: 41,
|
|
303
|
+
version: "6",
|
|
304
|
+
when: 1790195407572,
|
|
305
|
+
tag: "0041_many_marvel_boy",
|
|
306
|
+
breakpoints: true,
|
|
307
|
+
},
|
|
294
308
|
],
|
|
295
309
|
},
|
|
296
310
|
migrations: {
|
|
@@ -371,5 +385,9 @@ export const MIRROR_MIGRATIONS = {
|
|
|
371
385
|
"ALTER TABLE `fleet_activity` ADD `provider` text;--> statement-breakpoint\nALTER TABLE `fleet_activity` ADD `model` text;--> statement-breakpoint\nALTER TABLE `fleet_activity` ADD `effort` text;--> statement-breakpoint\nALTER TABLE `fleet_activity` ADD `routing_source` text;",
|
|
372
386
|
"0039_unknown_nebula":
|
|
373
387
|
"CREATE TABLE `flow_daily` (\n\t`repo_id` text NOT NULL,\n\t`day` text NOT NULL,\n\t`active_count` integer NOT NULL,\n\t`entries` integer NOT NULL,\n\t`exits_to_done` integer NOT NULL,\n\t`exits_to_canceled` integer NOT NULL,\n\t`stage_counts` text,\n\t`aging_p50_seconds` integer,\n\t`aging_p90_seconds` integer,\n\t`fleet_active_count` integer,\n\t`fleet_exits_to_done` integer,\n\t`fleet_cycle_p50_seconds` integer,\n\t`fleet_cycle_p90_seconds` integer,\n\t`interactive_active_count` integer,\n\t`interactive_exits_to_done` integer,\n\t`interactive_cycle_p50_seconds` integer,\n\t`interactive_cycle_p90_seconds` integer,\n\t`stalled_count` integer,\n\t`estimated_ticket_count` integer,\n\t`open_pr_count` integer,\n\t`open_pr_draft_count` integer,\n\t`prs_merged` integer,\n\t`pr_cycle_p50_seconds` integer,\n\t`pr_cycle_p90_seconds` integer,\n\t`pr_age_p50_seconds` integer,\n\t`pr_age_p90_seconds` integer,\n\t`computed_at` integer NOT NULL,\n\tPRIMARY KEY(`repo_id`, `day`)\n);\n--> statement-breakpoint\nCREATE INDEX `idx_flow_daily_day` ON `flow_daily` (`day`);",
|
|
388
|
+
"0040_ctc1885_workflow_state_name_backfill":
|
|
389
|
+
"-- CTC-1885 — re-stamp cached workflow-state NAMES from the authoritative workflow_states row, by\n-- stable state id.\n--\n-- ⭐ Deliberately a pure DATA migration in the SHARED bundle (the 0014/0031/0037 pattern): the DO\n-- and every host / OPFS replica apply the same MIRROR_MIGRATIONS through the same runner, so each\n-- store repairs its OWN rows locally. That converges the already-stale corpus WITHOUT emitting a\n-- change_log delta per historical row (migrations bypass appendChange), so the 200k-row / 7-day ring\n-- buffer is not flooded by the very backlog this repairs, and a replica that was OFFLINE at deploy\n-- time heals itself the moment it next opens and applies the bundle.\n--\n-- The defect: `issues.state` is denormalized at issue ingest and `team_workflow_mapping.\n-- linear_state_name` at mapping projection, while `workflow_states.name` is written by the\n-- workflow-state sweep. A Linear rename (Triage -> Intake) updated only the latter, so issues kept\n-- answering the OLD name (/api/v1/issues returns the column verbatim; the board keys columns by it)\n-- and the mapping kept displaying it. Runtime cascade (this ticket's reconcile change) covers FUTURE\n-- renames; THIS migration repairs every row already stale at upgrade time, including on stores the\n-- runtime never gets to replay.\n--\n-- Matching is by STABLE ID, never by the mutable old string (the table's own header: the name is a\n-- display snapshot, never a lookup key — a same-named row under a different state id must not move):\n-- • issues: `issues.state_id` -> `workflow_states.id`. The state id is globally unique (Linear\n-- UUID), which IS the team scoping — an issue's state belongs to exactly one team's workflow.\n-- • team_workflow_mapping: `linear_state_id` AND `team_id` both resolve. The mapping's team fence\n-- is kept because a Linear-to-Linear import can re-mint ids per team (CTC-403).\n-- Both updates are guarded by `ws.name IS NOT <cached>` so already-converged rows are untouched and\n-- a replay (execReplaySafe re-runs a statement after a crash between success and tag-record) is a\n-- no-op. `IS` / `IS NOT` keep every comparison NULL-safe.\n--\n-- ⛔ NO timestamps, revisions, or tombstones are rewritten: `updated_at`, `workflow_rev`, and\n-- `removed_at` survive exactly as stored. A state rename is provider-owned state metadata, not an\n-- issue mutation and not a tenant mapping decision; advancing those fields would corrupt freshness,\n-- board ranking, last-write-wins ordering, and the host workflow-rev receipt. Only the NAME fields\n-- converge. (Tombstoned mapping rows converge too — a dead row's display snapshot changing is\n-- unobservable and emits nothing; the runtime cascade, which DOES emit deltas, stays live-only.)\nUPDATE `issues`\n SET `state` = (SELECT `ws`.`name` FROM `workflow_states` `ws` WHERE `ws`.`id` = `issues`.`state_id`)\n WHERE EXISTS (\n SELECT 1 FROM `workflow_states` `ws`\n WHERE `ws`.`id` = `issues`.`state_id`\n AND `ws`.`name` IS NOT NULL\n AND `ws`.`name` IS NOT `issues`.`state`);\n--> statement-breakpoint\nUPDATE `team_workflow_mapping`\n SET `linear_state_name` = (SELECT `ws`.`name`\n FROM `workflow_states` `ws`\n WHERE `ws`.`id` = `team_workflow_mapping`.`linear_state_id`\n AND `ws`.`team_id` = `team_workflow_mapping`.`team_id`)\n WHERE EXISTS (\n SELECT 1 FROM `workflow_states` `ws`\n WHERE `ws`.`id` = `team_workflow_mapping`.`linear_state_id`\n AND `ws`.`team_id` = `team_workflow_mapping`.`team_id`\n AND `ws`.`name` IS NOT NULL\n AND `ws`.`name` IS NOT `team_workflow_mapping`.`linear_state_name`);\n",
|
|
390
|
+
"0041_many_marvel_boy":
|
|
391
|
+
'-- CTC-3123: re-key flow_daily from (repo_id, day) to (project_id, day), so a per-project Flow page\n-- never double-counts a repo that two projects share (ADR-0013\'s project_repos).\n--\n-- A table recreation, because SQLite cannot change a primary key in place. Two edits to the\n-- drizzle-kit output, both deliberate:\n-- • The copy writes `\'repo:\' || repo_id` into project_id. The repo-to-project map lives in the\n-- registry D1, not in this store, so a migration cannot know a row\'s project. The snapshot writer\n-- (apps/mirror/src/do/flow-snapshot.ts) re-stamps `repo:<owner>/<name>` rows to the real project\n-- id on its next pass. A repo with no registry project keeps the marker: still readable, never\n-- dropped, and counted by the flow route\'s `flow-daily-unkeyed-rows` log line.\n-- • No PRAGMA foreign_keys lines: flow_daily has no foreign keys, so toggling enforcement buys\n-- nothing, and no earlier migration in this bundle issues a pragma. The leading DROP of `__new_flow_daily` makes a replay after a\n-- partial run start clean instead of colliding on the half-built table.\nDROP TABLE IF EXISTS `__new_flow_daily`;--> statement-breakpoint\nCREATE TABLE `__new_flow_daily` (\n\t`project_id` text NOT NULL,\n\t`repo_id` text NOT NULL,\n\t`day` text NOT NULL,\n\t`active_count` integer NOT NULL,\n\t`entries` integer NOT NULL,\n\t`exits_to_done` integer NOT NULL,\n\t`exits_to_canceled` integer NOT NULL,\n\t`stage_counts` text,\n\t`aging_p50_seconds` integer,\n\t`aging_p90_seconds` integer,\n\t`fleet_active_count` integer,\n\t`fleet_exits_to_done` integer,\n\t`fleet_cycle_p50_seconds` integer,\n\t`fleet_cycle_p90_seconds` integer,\n\t`interactive_active_count` integer,\n\t`interactive_exits_to_done` integer,\n\t`interactive_cycle_p50_seconds` integer,\n\t`interactive_cycle_p90_seconds` integer,\n\t`stalled_count` integer,\n\t`estimated_ticket_count` integer,\n\t`open_pr_count` integer,\n\t`open_pr_draft_count` integer,\n\t`prs_merged` integer,\n\t`pr_cycle_p50_seconds` integer,\n\t`pr_cycle_p90_seconds` integer,\n\t`pr_age_p50_seconds` integer,\n\t`pr_age_p90_seconds` integer,\n\t`computed_at` integer NOT NULL,\n\tPRIMARY KEY(`project_id`, `day`)\n);\n--> statement-breakpoint\nINSERT INTO `__new_flow_daily`("project_id", "repo_id", "day", "active_count", "entries", "exits_to_done", "exits_to_canceled", "stage_counts", "aging_p50_seconds", "aging_p90_seconds", "fleet_active_count", "fleet_exits_to_done", "fleet_cycle_p50_seconds", "fleet_cycle_p90_seconds", "interactive_active_count", "interactive_exits_to_done", "interactive_cycle_p50_seconds", "interactive_cycle_p90_seconds", "stalled_count", "estimated_ticket_count", "open_pr_count", "open_pr_draft_count", "prs_merged", "pr_cycle_p50_seconds", "pr_cycle_p90_seconds", "pr_age_p50_seconds", "pr_age_p90_seconds", "computed_at") SELECT \'repo:\' || "repo_id", "repo_id", "day", "active_count", "entries", "exits_to_done", "exits_to_canceled", "stage_counts", "aging_p50_seconds", "aging_p90_seconds", "fleet_active_count", "fleet_exits_to_done", "fleet_cycle_p50_seconds", "fleet_cycle_p90_seconds", "interactive_active_count", "interactive_exits_to_done", "interactive_cycle_p50_seconds", "interactive_cycle_p90_seconds", "stalled_count", "estimated_ticket_count", "open_pr_count", "open_pr_draft_count", "prs_merged", "pr_cycle_p50_seconds", "pr_cycle_p90_seconds", "pr_age_p50_seconds", "pr_age_p90_seconds", "computed_at" FROM `flow_daily`;--> statement-breakpoint\nDROP TABLE `flow_daily`;--> statement-breakpoint\nALTER TABLE `__new_flow_daily` RENAME TO `flow_daily`;--> statement-breakpoint\nCREATE INDEX `idx_flow_daily_day` ON `flow_daily` (`day`);',
|
|
374
392
|
},
|
|
375
393
|
} as const;
|
package/src/mirror.ts
CHANGED
|
@@ -1495,8 +1495,8 @@ export const fleet_health_sends = sqliteTable(
|
|
|
1495
1495
|
* already carries, so broadcasting it account-wide would put a recomputable aggregate on every host
|
|
1496
1496
|
* replica and every browser OPFS DB for no reader.
|
|
1497
1497
|
*
|
|
1498
|
-
* ⚠️ NO `tenant_id` — the DO IS the tenant (this file's header). The
|
|
1499
|
-
*
|
|
1498
|
+
* ⚠️ NO `tenant_id` — the DO IS the tenant (this file's header). The key is (project_id, day) since
|
|
1499
|
+
* CTC-3123; it was (repo_id, day) before.
|
|
1500
1500
|
*
|
|
1501
1501
|
* ⭐ EVERY DURATION IS SECONDS, and every column that holds one says so in its name (`*_seconds`). The
|
|
1502
1502
|
* ticket's adaptive-rendering rule (minutes as the fleet improves) lives in the client; the store never
|
|
@@ -1505,6 +1505,15 @@ export const fleet_health_sends = sqliteTable(
|
|
|
1505
1505
|
export const flow_daily = sqliteTable(
|
|
1506
1506
|
"flow_daily",
|
|
1507
1507
|
{
|
|
1508
|
+
/**
|
|
1509
|
+
* CTC-3123: the registry project id (`projects.id`, e.g. `tenant-0:catalyst`), the row's key with
|
|
1510
|
+
* `day`. Keyed by project, not repo, so a repo shared by two projects (ADR-0013's `project_repos`)
|
|
1511
|
+
* never double-counts on a per-project Flow page. A row carried over from the repo-keyed table by
|
|
1512
|
+
* migration 0041 holds `repo:<owner>/<name>` until the snapshot writer re-stamps it
|
|
1513
|
+
* (`flow-snapshot.ts`); a repo with no registry project keeps that marker and stays readable.
|
|
1514
|
+
*/
|
|
1515
|
+
project_id: text("project_id").notNull(),
|
|
1516
|
+
/** `owner/name` of the project's repository. Descriptive since CTC-3123, no longer the key. */
|
|
1508
1517
|
repo_id: text("repo_id").notNull(),
|
|
1509
1518
|
/** UTC `YYYY-MM-DD` — `utcDayString`, never a raw timestamp (write-budget.ts's boundary rule). */
|
|
1510
1519
|
day: text("day").notNull(),
|
|
@@ -1540,5 +1549,5 @@ export const flow_daily = sqliteTable(
|
|
|
1540
1549
|
},
|
|
1541
1550
|
// The index lives in the Drizzle SSOT ONLY (NOT schema.sql) — migrate.ts's generated CREATE INDEX is
|
|
1542
1551
|
// replay-safe; schema.sql would break the adoption test (the same rule idx_issues_sort_order states).
|
|
1543
|
-
(t) => [primaryKey({ columns: [t.
|
|
1552
|
+
(t) => [primaryKey({ columns: [t.project_id, t.day] }), index("idx_flow_daily_day").on(t.day)],
|
|
1544
1553
|
);
|
|
@@ -45,6 +45,10 @@ export interface PhaseLifecycleFields {
|
|
|
45
45
|
artifactKey?: string;
|
|
46
46
|
durationMs?: number;
|
|
47
47
|
reason?: string;
|
|
48
|
+
/** CTC-1778 — one remediation round may carry several descriptive classes. They remain labels,
|
|
49
|
+
* not additional lifecycle points or admission keys. */
|
|
50
|
+
remediateClasses?: readonly string[];
|
|
51
|
+
remediationRound?: string;
|
|
48
52
|
/** CTC-1417 — the Linear team key. Optional here (an ergonomic caller-facing input, unlike
|
|
49
53
|
* TelemetryPoint's wire shape): when omitted, {@link buildPhaseLifecyclePoint} derives it from
|
|
50
54
|
* `ticket` via {@link teamFromTicket}. An explicit value always wins over the derivation. */
|
|
@@ -98,6 +102,12 @@ export const PHASE_LIFECYCLE_DETAIL_ORDER = [
|
|
|
98
102
|
"pr_number",
|
|
99
103
|
"artifact_key",
|
|
100
104
|
"reason",
|
|
105
|
+
// CTC-1778 — the bundled-remediation tokens, registered in the exact order
|
|
106
|
+
// `buildPhaseLifecyclePoint` emits them (remediation_round, then remediate_classes). Rule (2)
|
|
107
|
+
// above applies to both: unregistered, they are indistinguishable from a ladder digest pair and
|
|
108
|
+
// get swallowed into `reason`'s value.
|
|
109
|
+
"remediation_round",
|
|
110
|
+
"remediate_classes",
|
|
101
111
|
// Reserved — CTC-1771 Phase 5.
|
|
102
112
|
"failure_class",
|
|
103
113
|
"round",
|
|
@@ -163,6 +173,12 @@ export function buildPhaseLifecyclePoint(fields: PhaseLifecycleFields): Telemetr
|
|
|
163
173
|
...(fields.prNumber !== undefined ? [`pr_number=${fields.prNumber}`] : []),
|
|
164
174
|
...(fields.artifactKey !== undefined ? [`artifact_key=${fields.artifactKey}`] : []),
|
|
165
175
|
...(fields.reason !== undefined ? [`reason=${fields.reason}`] : []),
|
|
176
|
+
...(fields.remediationRound !== undefined
|
|
177
|
+
? [`remediation_round=${fields.remediationRound}`]
|
|
178
|
+
: []),
|
|
179
|
+
...(fields.remediateClasses !== undefined && fields.remediateClasses.length > 0
|
|
180
|
+
? [`remediate_classes=${[...new Set(fields.remediateClasses)].sort().join(",")}`]
|
|
181
|
+
: []),
|
|
166
182
|
];
|
|
167
183
|
return {
|
|
168
184
|
name: "phase.lifecycle",
|