@catalyst-cloud/schema 0.1.62 → 0.1.63

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@catalyst-cloud/schema",
3
- "version": "0.1.62",
3
+ "version": "0.1.63",
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",
@@ -305,6 +305,13 @@ export const MIRROR_MIGRATIONS = {
305
305
  tag: "0041_many_marvel_boy",
306
306
  breakpoints: true,
307
307
  },
308
+ {
309
+ idx: 42,
310
+ version: "6",
311
+ when: 1790382152574,
312
+ tag: "0042_ctc1858_fleet_activity_started_at_and_class",
313
+ breakpoints: true,
314
+ },
308
315
  ],
309
316
  },
310
317
  migrations: {
@@ -389,5 +396,7 @@ export const MIRROR_MIGRATIONS = {
389
396
  "-- 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
397
  "0041_many_marvel_boy":
391
398
  '-- 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\n-- `__new_flow_daily` clears a half-built copy while flow_daily still exists. If the old table\n-- was already dropped, applyMigrations first restores the copied table\'s name before replay.\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`);\n',
399
+ "0042_ctc1858_fleet_activity_started_at_and_class":
400
+ "ALTER TABLE `fleet_activity` ADD `started_at` integer;--> statement-breakpoint\nALTER TABLE `fleet_activity` ADD `duration_ms` integer;--> statement-breakpoint\nALTER TABLE `fleet_activity` ADD `container_class` text;",
392
401
  },
393
402
  } as const;
package/src/mirror.ts CHANGED
@@ -1190,6 +1190,24 @@ export const fleet_activity = sqliteTable(
1190
1190
  last_event_name: text("last_event_name"),
1191
1191
  last_event_ts: integer("last_event_ts"), // ms epoch — the reducer watermark
1192
1192
  updated_at: integer("updated_at"), // ms epoch — mirrors last_event_ts; upsertRow guard column
1193
+ // CTC-1858 — the phase TENURE's own span and the container it ran on, projected from attrs the
1194
+ // runner already publishes (worker.session.started's `ts`; `duration_ms` and `resource_profile`
1195
+ // off assignment telemetry). Declared last because ALTER TABLE ADD appends, so declared order
1196
+ // and physical order agree for a migrated table.
1197
+ //
1198
+ // ⛔ started_at / duration_ms are TENURE-SCOPED coalesce-forward, not plain coalesce-forward:
1199
+ // this table's key is (host_id, ticket), so one row is REUSED as a ticket walks phase to phase
1200
+ // on one host. Carrying a previous run's stamp into this one would render a duration spanning
1201
+ // both runs plus the gap. reduceFleetActivity drops it at EITHER of two tenure boundaries — an
1202
+ // incoming event naming a different non-null phase, or a re-dispatch of the SAME phase (a row
1203
+ // that already reached a disposition being put back to running, or handed a second phase
1204
+ // outcome). A `worker.session.resumed` is NEITHER: it is the same worker picking its own tenure
1205
+ // back up, and it keeps the measurements. container_class is plain coalesce-forward — it
1206
+ // describes the
1207
+ // CONTAINER, which is host-scoped, and it rides both the start and the terminal record.
1208
+ started_at: integer("started_at"), // ms epoch — worker.session.started's own instant
1209
+ duration_ms: integer("duration_ms"), // runner-monotonic phase duration; terminal records only
1210
+ container_class: text("container_class"), // an instance-TYPE label ("standard-4"), CONTAINER_INSTANCE_SPECS key
1193
1211
  },
1194
1212
  (t) => [primaryKey({ columns: [t.host_id, t.ticket] })],
1195
1213
  );