@catalyst-cloud/schema 0.1.52 → 0.1.54
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.
|
|
3
|
+
"version": "0.1.54",
|
|
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",
|
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.",
|
|
@@ -2188,6 +2216,20 @@ export const telemetryEventTypes = {
|
|
|
2188
2216
|
reason:
|
|
2189
2217
|
"A per-turn counter sample of the agent SDK loop — a gauge, not a discrete coordination fact.",
|
|
2190
2218
|
},
|
|
2219
|
+
// CTC-1385 — the fleet flow-metrics tracer bullet's two per-transition facts. `always`-sampled: each
|
|
2220
|
+
// occurrence is an individually meaningful ticket-/PR-level stage transition (the exact grain the
|
|
2221
|
+
// ship-or-languish cohorts and eval baselines are computed from), not a gauge or a retry-storm signal
|
|
2222
|
+
// — sampling one in a hundred would make a per-cohort probability meaningless.
|
|
2223
|
+
"flow.transition": {
|
|
2224
|
+
reason:
|
|
2225
|
+
"A ticket crossed a flow-board stage boundary — the per-transition fact the P(ship within N days), aging-WIP and stage-stuck metrics are computed from. Individually meaningful, not a sample.",
|
|
2226
|
+
sampling: { kind: "always" },
|
|
2227
|
+
},
|
|
2228
|
+
"flow.pr-transition": {
|
|
2229
|
+
reason:
|
|
2230
|
+
"A pull request crossed a lifecycle boundary (opened/ready/queued/merged/closed) — the PR-side counterpart to flow.transition, same always-sampled reasoning.",
|
|
2231
|
+
sampling: { kind: "always" },
|
|
2232
|
+
},
|
|
2191
2233
|
"github-feed.would-dispatch": {
|
|
2192
2234
|
reason:
|
|
2193
2235
|
"Same 'would-*' counterfactual shape as cloud-feed.would-dispatch, for the GitHub-sourced feed.",
|
package/src/index.ts
CHANGED
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
fleet_anomalies,
|
|
55
55
|
fleet_activity_rate,
|
|
56
56
|
fleet_health_sends,
|
|
57
|
+
flow_daily,
|
|
57
58
|
} from "./mirror.js";
|
|
58
59
|
|
|
59
60
|
export * from "./mirror.js";
|
|
@@ -92,6 +93,8 @@ export * from "./events/index.js";
|
|
|
92
93
|
export * from "./telemetry/point.js";
|
|
93
94
|
// CTC-1261 — the phase.lifecycle family's closed status vocabulary and shared point builder.
|
|
94
95
|
export * from "./telemetry/phase-lifecycle-point.js";
|
|
96
|
+
// CTC-1385 — the flow.transition / flow.pr-transition family's closed vocabulary and point builders.
|
|
97
|
+
export * from "./telemetry/flow-transition-point.js";
|
|
95
98
|
|
|
96
99
|
/** Every table in the Mirror DO store — pass as `drizzle(storage, { schema: mirrorSchema })`. */
|
|
97
100
|
export const mirrorSchema = {
|
|
@@ -167,6 +170,10 @@ export const mirrorSchema = {
|
|
|
167
170
|
// consumer should ever see over the feed).
|
|
168
171
|
fleet_activity_rate,
|
|
169
172
|
fleet_health_sends,
|
|
173
|
+
// CTC-1385: the per-(repo,day) flow-metrics snapshot. A real table the DO creates and the migration
|
|
174
|
+
// bundle must produce, but DELIBERATELY ABSENT from `feedEntityTables` below — see the no-feed
|
|
175
|
+
// rationale on the table definition in ./mirror.ts.
|
|
176
|
+
flow_daily,
|
|
170
177
|
} as const;
|
|
171
178
|
|
|
172
179
|
/**
|
|
@@ -284,6 +284,20 @@ export const MIRROR_MIGRATIONS = {
|
|
|
284
284
|
tag: "0038_ctc1855_fleet_activity_routing",
|
|
285
285
|
breakpoints: true,
|
|
286
286
|
},
|
|
287
|
+
{
|
|
288
|
+
idx: 39,
|
|
289
|
+
version: "6",
|
|
290
|
+
when: 1789132086184,
|
|
291
|
+
tag: "0039_unknown_nebula",
|
|
292
|
+
breakpoints: true,
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
idx: 40,
|
|
296
|
+
version: "6",
|
|
297
|
+
when: 1789188558404,
|
|
298
|
+
tag: "0040_ctc1885_workflow_state_name_backfill",
|
|
299
|
+
breakpoints: true,
|
|
300
|
+
},
|
|
287
301
|
],
|
|
288
302
|
},
|
|
289
303
|
migrations: {
|
|
@@ -362,5 +376,9 @@ export const MIRROR_MIGRATIONS = {
|
|
|
362
376
|
"-- CTC-1597 — rename the persisted workflow slot `triage` -> `intake`.\n--\n-- ⭐ Deliberately a DATA migration in the SHARED bundle (the 0014/0031 pattern): the DO and every\n-- host / OPFS replica apply the same MIRROR_MIGRATIONS through the same runner, so each side renames\n-- its OWN rows locally. That converges every replica WITHOUT emitting a change_log delta per row\n-- (migrations bypass appendChange), so no ring-buffer flood.\n--\n-- ⚠️ THE REGISTRY D1 IS THE AUTHORITY and gets its own migration\n-- (apps/mirror/src/registry/migrations/20260903T1928_workflow_slot_intake.sql); this one exists\n-- because a D1-only rewrite bumps no `workflow_rev` and pushes nothing, so a DO that is never\n-- re-saved would sit on the old value forever. THIS migration is what makes the rename correct on\n-- every DO/replica, including one offline at merge time. There is no separate re-projection script:\n-- an already-connected browser/host that needs to see the rename before its own next open converges\n-- the ordinary way — an operator re-saving that team's stage mapping calls the existing full-replace\n-- projection handler (`handleWorkflowMappingUpsert`, MirrorDO.ts), which DOES emit a change_log\n-- delta. No new production route was added for a one-time rename.\n--\n-- PK-safe: (team_id, slot) — and the PK does NOT consider `removed_at`, so a SOFT-DELETED `intake`\n-- row still occupies the slot. Drop a stale `triage` row for a team that already holds ANY `intake`\n-- row — live or soft-deleted — then rename what is left. Idempotent under execReplaySafe replay — a\n-- row already renamed to `intake` no longer matches `slot = 'triage'` on a re-run.\n--\n-- ⛔ CTC-1597 remediate (C2): an earlier draft of the EXISTS guard filtered `removed_at IS NULL`,\n-- reasoning that a soft-deleted `intake` row should never cause a still-live `triage` row to be\n-- dropped instead of renamed. That reasoning had it backwards — the PK collision this guard exists\n-- to prevent happens precisely BECAUSE `removed_at` is not part of the primary key: a host that\n-- receives a soft-deleted `intake` row via replication (from a DO that already renamed and was then\n-- unmapped) before it has applied this migration locally still has its own live `triage` row: the\n-- DELETE's old filter left the `triage` row in place, and the following blind UPDATE then tried to\n-- rename it to a slot value a row already occupies, violating the PK and aborting the whole bundle.\n-- Matching on ANY `intake` row (regardless of `removed_at`) fixes it: the stale `triage` row is\n-- dropped, not renamed, whenever the slot is already spoken for.\n--\n-- ⛔ CTC-1597 remediate (validate-plan finding 1) — a `triage`-slot row whose OWN mapped state is\n-- ITSELF type='triage' (Linear's reserved Triage inbox, legally name-matched into the OLD\n-- `RECOMMENDED_STAGES` entry before this ticket) must NOT be renamed blind: `SLOT_STATE_TYPES.intake`\n-- no longer accepts that type (Scope B), so renaming it in place would silently flip the team's\n-- `types_compatible` check from pass to a blocking fail with no data fix and no recourse — the write\n-- path's own validation (the same narrowing) would refuse a re-save of the identical row. Soft-delete\n-- it instead, via `removed_at` — this table's own convention for \"this slot is unmapped\" (see the\n-- table's doc comment in packages/schema/src/mirror.ts) — so the team lands in the ordinary,\n-- already-actionable \"needs a stage\" state a brand-new team starts in, which Scope C's per-team\n-- runbook resolves. Runs BEFORE the final rename so no live-or-dead row anywhere keeps the retired\n-- `triage` slot value; guarded by `removed_at IS NULL` so a replay is a no-op.\nUPDATE `team_workflow_mapping`\n SET `removed_at` = CAST(unixepoch() AS INTEGER) * 1000\n WHERE `slot` = 'triage' AND `linear_state_type` = 'triage' AND `removed_at` IS NULL;\n--> statement-breakpoint\nDELETE FROM `team_workflow_mapping`\n WHERE `slot` = 'triage'\n AND EXISTS (SELECT 1 FROM `team_workflow_mapping` m2\n WHERE m2.`team_id` = `team_workflow_mapping`.`team_id` AND m2.`slot` = 'intake');\n--> statement-breakpoint\nUPDATE `team_workflow_mapping` SET `slot` = 'intake' WHERE `slot` = 'triage';\n",
|
|
363
377
|
"0038_ctc1855_fleet_activity_routing":
|
|
364
378
|
"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;",
|
|
379
|
+
"0039_unknown_nebula":
|
|
380
|
+
"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`);",
|
|
381
|
+
"0040_ctc1885_workflow_state_name_backfill":
|
|
382
|
+
"-- 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",
|
|
365
383
|
},
|
|
366
384
|
} as const;
|
package/src/mirror.ts
CHANGED
|
@@ -1484,3 +1484,61 @@ export const fleet_health_sends = sqliteTable(
|
|
|
1484
1484
|
},
|
|
1485
1485
|
(t) => [primaryKey({ columns: [t.host_id, t.status, t.status_changed_at_ms] })],
|
|
1486
1486
|
);
|
|
1487
|
+
|
|
1488
|
+
/**
|
|
1489
|
+
* CTC-1385 — one FLOW SNAPSHOT per (repo_id, day). A derived rollup, recomputed from `issue_history` /
|
|
1490
|
+
* `pull_requests` each reconcile pass and overwritten in place — never a hand-edited or externally
|
|
1491
|
+
* written row.
|
|
1492
|
+
*
|
|
1493
|
+
* ⛔ HUB-READ-ONLY — deliberately in NEITHER `feedEntityTables` NOR `nonFeedEntityTables` (index.ts),
|
|
1494
|
+
* the same posture as `pull_sweeps` / `pr_ancillary_backfill`. It is recomputable from tables the feed
|
|
1495
|
+
* already carries, so broadcasting it account-wide would put a recomputable aggregate on every host
|
|
1496
|
+
* replica and every browser OPFS DB for no reader.
|
|
1497
|
+
*
|
|
1498
|
+
* ⚠️ NO `tenant_id` — the DO IS the tenant (this file's header). The ticket phrases the key as
|
|
1499
|
+
* (tenant, repository, day); inside a tenant's own DO that is exactly (repo_id, day). Nothing dropped.
|
|
1500
|
+
*
|
|
1501
|
+
* ⭐ EVERY DURATION IS SECONDS, and every column that holds one says so in its name (`*_seconds`). The
|
|
1502
|
+
* ticket's adaptive-rendering rule (minutes as the fleet improves) lives in the client; the store never
|
|
1503
|
+
* picks a unit.
|
|
1504
|
+
*/
|
|
1505
|
+
export const flow_daily = sqliteTable(
|
|
1506
|
+
"flow_daily",
|
|
1507
|
+
{
|
|
1508
|
+
repo_id: text("repo_id").notNull(),
|
|
1509
|
+
/** UTC `YYYY-MM-DD` — `utcDayString`, never a raw timestamp (write-budget.ts's boundary rule). */
|
|
1510
|
+
day: text("day").notNull(),
|
|
1511
|
+
active_count: integer("active_count").notNull(),
|
|
1512
|
+
entries: integer("entries").notNull(),
|
|
1513
|
+
exits_to_done: integer("exits_to_done").notNull(),
|
|
1514
|
+
exits_to_canceled: integer("exits_to_canceled").notNull(),
|
|
1515
|
+
/** JSON: `{ "<stateKey>": count }` over the active-board tickets currently in that state. */
|
|
1516
|
+
stage_counts: text("stage_counts"),
|
|
1517
|
+
aging_p50_seconds: integer("aging_p50_seconds"),
|
|
1518
|
+
aging_p90_seconds: integer("aging_p90_seconds"),
|
|
1519
|
+
// The same figures, split by delivery mode (CTC-1385's delivery-mode rule).
|
|
1520
|
+
fleet_active_count: integer("fleet_active_count"),
|
|
1521
|
+
fleet_exits_to_done: integer("fleet_exits_to_done"),
|
|
1522
|
+
fleet_cycle_p50_seconds: integer("fleet_cycle_p50_seconds"),
|
|
1523
|
+
fleet_cycle_p90_seconds: integer("fleet_cycle_p90_seconds"),
|
|
1524
|
+
interactive_active_count: integer("interactive_active_count"),
|
|
1525
|
+
interactive_exits_to_done: integer("interactive_exits_to_done"),
|
|
1526
|
+
interactive_cycle_p50_seconds: integer("interactive_cycle_p50_seconds"),
|
|
1527
|
+
interactive_cycle_p90_seconds: integer("interactive_cycle_p90_seconds"),
|
|
1528
|
+
stalled_count: integer("stalled_count"),
|
|
1529
|
+
estimated_ticket_count: integer("estimated_ticket_count"),
|
|
1530
|
+
// The GitHub pull-request addendum (added 2026-09-01) — same repo/day grain, no new ingestion:
|
|
1531
|
+
// sourced from the already-mirrored `pull_requests` table.
|
|
1532
|
+
open_pr_count: integer("open_pr_count"),
|
|
1533
|
+
open_pr_draft_count: integer("open_pr_draft_count"),
|
|
1534
|
+
prs_merged: integer("prs_merged"),
|
|
1535
|
+
pr_cycle_p50_seconds: integer("pr_cycle_p50_seconds"),
|
|
1536
|
+
pr_cycle_p90_seconds: integer("pr_cycle_p90_seconds"),
|
|
1537
|
+
pr_age_p50_seconds: integer("pr_age_p50_seconds"),
|
|
1538
|
+
pr_age_p90_seconds: integer("pr_age_p90_seconds"),
|
|
1539
|
+
computed_at: integer("computed_at").notNull(), // ms epoch — when this pass wrote the row
|
|
1540
|
+
},
|
|
1541
|
+
// The index lives in the Drizzle SSOT ONLY (NOT schema.sql) — migrate.ts's generated CREATE INDEX is
|
|
1542
|
+
// replay-safe; schema.sql would break the adoption test (the same rule idx_issues_sort_order states).
|
|
1543
|
+
(t) => [primaryKey({ columns: [t.repo_id, t.day] }), index("idx_flow_daily_day").on(t.day)],
|
|
1544
|
+
);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// flow-transition-point.ts — CTC-1385. The `flow.transition` / `flow.pr-transition` telemetry family's
|
|
2
|
+
// closed vocabulary and shared TelemetryPoint builders, modeled line-for-line on
|
|
3
|
+
// ./phase-lifecycle-point.ts: a closed enum, a throwing guard (never a falsy sentinel — the same
|
|
4
|
+
// posture assertEmittable takes for the event NAME), and one builder per point kind.
|
|
5
|
+
//
|
|
6
|
+
// ⚠️ EVERY DIMENSION IS AN IDENTIFIER OR AN ENUM — no ticket titles, no PR titles, no branch names, no
|
|
7
|
+
// tenant prose (ADR-0048 binding condition 1). `ticket`/`repository` are identifiers; every other field
|
|
8
|
+
// is a closed vocabulary member or a number.
|
|
9
|
+
|
|
10
|
+
import type { TelemetryPoint } from "./point.js";
|
|
11
|
+
import { teamFromTicket } from "./phase-lifecycle-point.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The PR-transition kind vocabulary. `queued` is a MEMBER even though nothing emits it yet (D18 — no
|
|
15
|
+
* mirrored table carries a "PR entered the merge queue at T" timestamp today); documented here so the
|
|
16
|
+
* follow-up that adds queue-entry ingestion needs no vocabulary change.
|
|
17
|
+
*/
|
|
18
|
+
export const PR_TRANSITION_KINDS = ["opened", "ready", "queued", "merged", "closed"] as const;
|
|
19
|
+
export type PrTransitionKind = (typeof PR_TRANSITION_KINDS)[number];
|
|
20
|
+
|
|
21
|
+
function assertKnownPrTransition(kind: string): asserts kind is PrTransitionKind {
|
|
22
|
+
if (!(PR_TRANSITION_KINDS as readonly string[]).includes(kind)) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`flow.pr-transition refuses kind "${kind}": not one of ${PR_TRANSITION_KINDS.join("|")} ` +
|
|
25
|
+
`(packages/schema/src/telemetry/flow-transition-point.ts is the one closed vocabulary)`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FlowTransitionFields {
|
|
31
|
+
ticket: string;
|
|
32
|
+
repository: string;
|
|
33
|
+
fromStage: string;
|
|
34
|
+
toStage: string;
|
|
35
|
+
secondsInStage: number;
|
|
36
|
+
deliveryMode: "fleet" | "interactive";
|
|
37
|
+
estimate: number | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Build one `flow.transition` TelemetryPoint — a ticket crossing a flow-board stage boundary. */
|
|
41
|
+
export function buildFlowTransitionPoint(fields: FlowTransitionFields): TelemetryPoint {
|
|
42
|
+
const detailParts = [
|
|
43
|
+
`ticket=${fields.ticket}`,
|
|
44
|
+
`repository=${fields.repository}`,
|
|
45
|
+
`from_stage=${fields.fromStage}`,
|
|
46
|
+
`to_stage=${fields.toStage}`,
|
|
47
|
+
`seconds_in_stage=${fields.secondsInStage}`,
|
|
48
|
+
`delivery_mode=${fields.deliveryMode}`,
|
|
49
|
+
...(fields.estimate != null ? [`estimate=${fields.estimate}`] : []),
|
|
50
|
+
];
|
|
51
|
+
return {
|
|
52
|
+
name: "flow.transition",
|
|
53
|
+
service: "catalyst-cloud.mirror",
|
|
54
|
+
outcome: "ok",
|
|
55
|
+
detail: detailParts.join(" "),
|
|
56
|
+
traceId: `${fields.ticket}:${fields.fromStage}:${fields.toStage}`,
|
|
57
|
+
durationMs: null,
|
|
58
|
+
count: 1,
|
|
59
|
+
projectId: null,
|
|
60
|
+
repo: fields.repository,
|
|
61
|
+
team: teamFromTicket(fields.ticket),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface PrTransitionFields {
|
|
66
|
+
repository: string;
|
|
67
|
+
prNumber: number;
|
|
68
|
+
kind: PrTransitionKind;
|
|
69
|
+
secondsSincePrevious: number;
|
|
70
|
+
deliveryMode: "fleet" | "interactive";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Build one `flow.pr-transition` TelemetryPoint. Throws (never a falsy sentinel) on an unrecognized
|
|
74
|
+
* `kind` — the same posture `assertKnownStatus` takes in phase-lifecycle-point.ts. */
|
|
75
|
+
export function buildPrTransitionPoint(fields: PrTransitionFields): TelemetryPoint {
|
|
76
|
+
assertKnownPrTransition(fields.kind);
|
|
77
|
+
const detailParts = [
|
|
78
|
+
`repository=${fields.repository}`,
|
|
79
|
+
`pr_number=${fields.prNumber}`,
|
|
80
|
+
`kind=${fields.kind}`,
|
|
81
|
+
`seconds_since_previous=${fields.secondsSincePrevious}`,
|
|
82
|
+
`delivery_mode=${fields.deliveryMode}`,
|
|
83
|
+
];
|
|
84
|
+
return {
|
|
85
|
+
name: "flow.pr-transition",
|
|
86
|
+
service: "catalyst-cloud.mirror",
|
|
87
|
+
outcome: "ok",
|
|
88
|
+
detail: detailParts.join(" "),
|
|
89
|
+
traceId: `${fields.repository}:${fields.prNumber}:${fields.kind}`,
|
|
90
|
+
durationMs: null,
|
|
91
|
+
count: 1,
|
|
92
|
+
projectId: null,
|
|
93
|
+
repo: fields.repository,
|
|
94
|
+
team: null,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
@@ -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",
|