@catalyst-cloud/schema 0.1.49 → 0.1.50

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.49",
3
+ "version": "0.1.50",
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",
@@ -62,6 +62,88 @@ export function teamFromTicket(ticket: string): string | null {
62
62
  return m === null ? null : m[1]!;
63
63
  }
64
64
 
65
+ /**
66
+ * CTC-1776 — THE ONE DOCUMENTED ORDER of `detail`'s `key=` tokens, and the reason there is one.
67
+ *
68
+ * `detail` is an APPEND-ONLY, fixed-order sequence so an operator's `blob5 LIKE '%status=…%phase=…%'`
69
+ * predicate keeps matching as fields are added (docs/devops.md's own recipe is written against it).
70
+ * A new optional field goes at the END of `buildPhaseLifecyclePoint`'s array AND at the end of this
71
+ * list — never inserted in the middle.
72
+ *
73
+ * `failure_class` and `round` are RESERVED here, ahead of CTC-1771 building them, so that ticket's
74
+ * fields land after `reason=` by construction rather than by anyone remembering to. The subsequence
75
+ * test in this module's spec is what enforces it.
76
+ *
77
+ * ⛔ `reason` is the ONE token whose value may contain spaces and its own `key=value` pairs — a
78
+ * validate row's `reason=ladder:plan-conformance=PASS type-safety=FAIL …` digest (CTC-1541/CTC-1776).
79
+ * A naive space-split therefore sees the digest's `step=verdict` pairs as fields of their own, which
80
+ * is exactly what the round-1 subsequence test missed (CTC-1776 validate round 2, CR-C). Two rules
81
+ * follow. (1) `reason` renders LAST of the free-text-capable tokens, so everything after it up to
82
+ * the next key REGISTERED HERE belongs to its value — a reader (the spec's `parseDetail`, an
83
+ * operator's `LIKE`) can only tell a digest pair from a field by consulting this list. (2) A field
84
+ * appended after `reason` MUST be registered here first: an unregistered one is indistinguishable
85
+ * from a digest token and is swallowed into `reason`'s value, which the spec's round-trip assertion
86
+ * turns into a red test rather than a silent census skew.
87
+ */
88
+ export const PHASE_LIFECYCLE_DETAIL_ORDER = [
89
+ "status",
90
+ "ticket",
91
+ "phase",
92
+ "nonce",
93
+ "executor",
94
+ "substrate",
95
+ "provider",
96
+ "branch",
97
+ "base_sha",
98
+ "pr_number",
99
+ "artifact_key",
100
+ "reason",
101
+ // Reserved — CTC-1771 Phase 5.
102
+ "failure_class",
103
+ "round",
104
+ ] as const;
105
+
106
+ /** CTC-1541/CTC-1776 — the `reason=` value a validate row carries for its per-step ladder digest.
107
+ * Used by BOTH the succeeded-receipt site and the failed-release site in MirrorDO.ts, so "exactly
108
+ * as a succeeded row does" is a shared function rather than two string literals. */
109
+ export const LADDER_REASON_PREFIX = "ladder:";
110
+ export function ladderReason(digest: string): string {
111
+ return `${LADDER_REASON_PREFIX}${digest}`;
112
+ }
113
+
114
+ /** CTC-1776 — the `reason=` value for a validate row whose ladder block could not be parsed at all.
115
+ * Deliberately NOT a `ladder:` digest: five UNREPORTED verdicts read as a verdict set, and the
116
+ * census must count "I could not look" apart from a real FAIL. Bare, with no cause suffix — the
117
+ * five causes are named in the runner's own log line (loop.ts), keyed by ticket and phase. */
118
+ export const VALIDATION_LADDER_UNPARSED_REASON = "validation-ladder-unparsed";
119
+
120
+ /** CTC-1776 — the one verdict that means "I could not look", mirrored from `LADDER_VERDICTS`
121
+ * (`apps/runner/src/validation-ladder.ts`); this package stays runner-free, same posture
122
+ * `apps/mirror/src/artifacts/relay-receipt.ts` already takes for the ladder's other bounds. */
123
+ export const UNREPORTED_LADDER_VERDICT = "UNREPORTED";
124
+
125
+ /**
126
+ * CTC-1776 remediate (validate round 1, CR-3) — is every `step=verdict` token in an already-rendered
127
+ * digest `UNREPORTED`?
128
+ *
129
+ * The succeeded-receipt site answers the same question from the parsed `RelayReceiptLadder` it holds;
130
+ * the FAILED-release site only ever holds the rendered digest string the runner wrote, so it needs
131
+ * this form. Both then map to {@link VALIDATION_LADDER_UNPARSED_REASON} rather than publishing five
132
+ * UNREPORTED verdicts as if they were a verdict set — the census has to count "I could not look"
133
+ * apart from a real FAIL on BOTH row statuses, not just on `succeeded`.
134
+ *
135
+ * An empty digest is `false`, never a vacuous `true`: `[].every(…)` is the exact shape AGENTS.md's
136
+ * false-clean-result rule names, and a summary with no tokens at all is not evidence of anything.
137
+ */
138
+ export function isAllUnreportedLadderDigest(digest: string): boolean {
139
+ const tokens = digest.split(/\s+/).filter((token) => token.length > 0);
140
+ if (tokens.length === 0) return false;
141
+ return tokens.every((token) => {
142
+ const eq = token.lastIndexOf("=");
143
+ return eq !== -1 && token.slice(eq + 1) === UNREPORTED_LADDER_VERDICT;
144
+ });
145
+ }
146
+
65
147
  /**
66
148
  * Build one `phase.lifecycle` TelemetryPoint. Throws (never a falsy sentinel) on an unrecognized
67
149
  * `status` — the same posture as `assertEmittable` for the event name.