@bli-cockpit/telemetry-core 0.1.14 → 0.1.16

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.
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Fold a label to its comparison form.
3
+ *
4
+ * Case and surrounding whitespace are not identity — `Brandon`, `brandon` and
5
+ * ` brandon ` are one actor, and treating them as three would defeat the whole
6
+ * point of clustering. Nothing else is touched. In particular no punctuation is
7
+ * stripped and no prefix is trimmed, because every such rule is a guess about
8
+ * what two labels have in common, and guessing is exactly what the person
9
+ * resolver refuses to do. This is the same case-fold-then-compare the resolver
10
+ * itself uses, kept deliberately as dumb as that one.
11
+ */
12
+ export declare function normalizeActorLabel(rawLabel: string): string;
13
+ /**
14
+ * The stable identity of an actor nobody can name yet.
15
+ *
16
+ * Derived from the label alone, so every event bearing that label — across
17
+ * files, across sources, across backfill runs, months apart — clusters under
18
+ * one id. That is what lets a person later say "this one is Brandon" once and
19
+ * have it mean something for all of them.
20
+ *
21
+ * The label kind is deliberately NOT part of the input. The same human appears
22
+ * as a queue field in one artifact and a brief variable in another; folding the
23
+ * kind in would split one stranger into several and put the corpus right back
24
+ * where it started.
25
+ */
26
+ export declare function unresolvedActorId(rawLabel: string): string;
27
+ export interface DecisionEventIdInput {
28
+ captureSource: string;
29
+ /**
30
+ * Hash of exactly the bytes that make up this one decision; null for a live
31
+ * hook firing, which has no artifact at all.
32
+ *
33
+ * "This one decision" and not "the file it arrived in", which is the whole
34
+ * point. A verdict JSON holds one decision, so its record is the file and the
35
+ * two hashes coincide. `merge-log.jsonl` holds thousands and gains another
36
+ * every time somebody merges, so its record is the single line — hashing the
37
+ * file there would change every id in it on the next append, and a re-import
38
+ * would write a second copy of the ledger instead of updating the rows it
39
+ * already has.
40
+ *
41
+ * Which snapshot of which file the record came out of is still worth knowing;
42
+ * it is kept as `source_artifact_hash_sha256`, beside `source_line_number`, as
43
+ * provenance. Neither is an identity input. Conflating the two jobs in one
44
+ * field is what made a growing ledger un-re-importable.
45
+ */
46
+ sourceRecordHashSha256: string | null;
47
+ kind: string;
48
+ /**
49
+ * Only for a live firing, which has no artifact to hash and no line to point
50
+ * at. Two hook firings a second apart are two events, and without this they
51
+ * would collapse into one.
52
+ */
53
+ occurredAt?: string | null;
54
+ /** Also live-firing only: same hook, same instant, different subject. */
55
+ subjectDiscriminator?: string | null;
56
+ }
57
+ /**
58
+ * The id of one decision event.
59
+ *
60
+ * Three consequences worth stating. Re-running the backfill is idempotent,
61
+ * because the same record always yields the same id no matter how much the file
62
+ * around it has grown or how far down it has been pushed. An *edited* record
63
+ * produces a new id rather than mutating an old one, which keeps the store
64
+ * append-only in the same way jarvis_corrections is — history is added to, never
65
+ * rewritten. And two byte-identical records collapse onto one row, which is the
66
+ * intended reading: in a ledger, identical bytes mean a hook wrote the same
67
+ * decision twice. If that is ever false the ledger format is underspecified, and
68
+ * the fix belongs there rather than in a tiebreaker here.
69
+ *
70
+ * The line number is deliberately absent. It is the one part of a record's
71
+ * position that a rotation or a compaction can change without the decision
72
+ * itself changing at all, and an id that moves when a file is renumbered is
73
+ * worse than a duplicate: it silently lands on some other decision's row.
74
+ */
75
+ export declare function decisionEventId(input: DecisionEventIdInput): string;
@@ -0,0 +1,79 @@
1
+ // BLI-2425: the two ids a decision event depends on, derived the same way by
2
+ // every writer.
3
+ //
4
+ // Both are deterministic on purpose, and for the same reason: the historical
5
+ // backfill will be re-run. A hook that fires twice, a backfill script that dies
6
+ // halfway and starts over, a corrected parser replayed against the same files —
7
+ // all of those must land on the same rows rather than a second copy. Determinism
8
+ // is what makes "ingest is idempotent" a property of the data instead of a
9
+ // promise about the code.
10
+ //
11
+ // This lives beside the schema rather than in each writer because two writers
12
+ // deriving an id two ways is the same bug as two resolvers deciding who someone
13
+ // is: it produces two answers to a question that has one.
14
+ import { createHash } from "node:crypto";
15
+ /** How much of the digest we keep. Collision risk at 32 hex is not a concern at
16
+ * a corpus of thousands, and short ids stay readable in a URL and a log line. */
17
+ const ID_HEX_LENGTH = 32;
18
+ function sha256Hex(value) {
19
+ return createHash("sha256").update(value, "utf8").digest("hex");
20
+ }
21
+ /**
22
+ * Fold a label to its comparison form.
23
+ *
24
+ * Case and surrounding whitespace are not identity — `Brandon`, `brandon` and
25
+ * ` brandon ` are one actor, and treating them as three would defeat the whole
26
+ * point of clustering. Nothing else is touched. In particular no punctuation is
27
+ * stripped and no prefix is trimmed, because every such rule is a guess about
28
+ * what two labels have in common, and guessing is exactly what the person
29
+ * resolver refuses to do. This is the same case-fold-then-compare the resolver
30
+ * itself uses, kept deliberately as dumb as that one.
31
+ */
32
+ export function normalizeActorLabel(rawLabel) {
33
+ return rawLabel.trim().toLowerCase();
34
+ }
35
+ /**
36
+ * The stable identity of an actor nobody can name yet.
37
+ *
38
+ * Derived from the label alone, so every event bearing that label — across
39
+ * files, across sources, across backfill runs, months apart — clusters under
40
+ * one id. That is what lets a person later say "this one is Brandon" once and
41
+ * have it mean something for all of them.
42
+ *
43
+ * The label kind is deliberately NOT part of the input. The same human appears
44
+ * as a queue field in one artifact and a brief variable in another; folding the
45
+ * kind in would split one stranger into several and put the corpus right back
46
+ * where it started.
47
+ */
48
+ export function unresolvedActorId(rawLabel) {
49
+ const normalized = normalizeActorLabel(rawLabel);
50
+ return `ua_${sha256Hex(`decision-actor:${normalized}`).slice(0, ID_HEX_LENGTH)}`;
51
+ }
52
+ /**
53
+ * The id of one decision event.
54
+ *
55
+ * Three consequences worth stating. Re-running the backfill is idempotent,
56
+ * because the same record always yields the same id no matter how much the file
57
+ * around it has grown or how far down it has been pushed. An *edited* record
58
+ * produces a new id rather than mutating an old one, which keeps the store
59
+ * append-only in the same way jarvis_corrections is — history is added to, never
60
+ * rewritten. And two byte-identical records collapse onto one row, which is the
61
+ * intended reading: in a ledger, identical bytes mean a hook wrote the same
62
+ * decision twice. If that is ever false the ledger format is underspecified, and
63
+ * the fix belongs there rather than in a tiebreaker here.
64
+ *
65
+ * The line number is deliberately absent. It is the one part of a record's
66
+ * position that a rotation or a compaction can change without the decision
67
+ * itself changing at all, and an id that moves when a file is renumbered is
68
+ * worse than a duplicate: it silently lands on some other decision's row.
69
+ */
70
+ export function decisionEventId(input) {
71
+ const parts = [
72
+ input.captureSource,
73
+ input.sourceRecordHashSha256 ?? "",
74
+ input.kind,
75
+ input.occurredAt ?? "",
76
+ input.subjectDiscriminator ?? "",
77
+ ];
78
+ return `de_${sha256Hex(parts.join("|")).slice(0, ID_HEX_LENGTH)}`;
79
+ }
@@ -0,0 +1,612 @@
1
+ import { z } from "zod";
2
+ export declare const DECISION_EVENT_ENVELOPE_VERSION = "decision-event.v1";
3
+ /** A git commit sha. Sha256Schema is 64 hex and does not fit a 40-hex commit. */
4
+ export declare const CommitShaSchema: z.ZodString;
5
+ export declare const DecisionEventKindSchema: z.ZodEnum<{
6
+ delegation_issued: "delegation_issued";
7
+ review_verdict: "review_verdict";
8
+ human_approval: "human_approval";
9
+ intervention: "intervention";
10
+ takeover: "takeover";
11
+ rule_fired: "rule_fired";
12
+ }>;
13
+ export type DecisionEventKind = z.infer<typeof DecisionEventKindSchema>;
14
+ /**
15
+ * Which artifact this event was read out of.
16
+ *
17
+ * Deliberately its own enum rather than a member added to CaptureSourceSchema.
18
+ * That enum describes what a collector watched on a coding machine; these are
19
+ * files a manager's harness wrote as a side effect of managing. Sharing the
20
+ * enum would imply a shared envelope, and the whole reason this path exists is
21
+ * that decision records have no session, no work context and no capture
22
+ * provenance to fill.
23
+ */
24
+ export declare const DecisionCaptureSourceSchema: z.ZodEnum<{
25
+ claude_state_verdict_json: "claude_state_verdict_json";
26
+ claude_state_queue_jsonl: "claude_state_queue_jsonl";
27
+ claude_state_worker_brief: "claude_state_worker_brief";
28
+ claude_state_retro_jsonl: "claude_state_retro_jsonl";
29
+ claude_state_bypass_log: "claude_state_bypass_log";
30
+ claude_state_merge_log: "claude_state_merge_log";
31
+ hook_runtime: "hook_runtime";
32
+ }>;
33
+ export type DecisionCaptureSource = z.infer<typeof DecisionCaptureSourceSchema>;
34
+ /** Which field of the source artifact produced this label. */
35
+ export declare const ActorLabelKindSchema: z.ZodEnum<{
36
+ email: "email";
37
+ github_login: "github_login";
38
+ linear_email: "linear_email";
39
+ linear_user_id: "linear_user_id";
40
+ reviewer_session: "reviewer_session";
41
+ hook_operator: "hook_operator";
42
+ queue_field: "queue_field";
43
+ brief_variable: "brief_variable";
44
+ }>;
45
+ export type ActorLabelKind = z.infer<typeof ActorLabelKindSchema>;
46
+ /** Whether the thing that acted was a person or something a person ran. */
47
+ export declare const ActorClassSchema: z.ZodEnum<{
48
+ human: "human";
49
+ agent: "agent";
50
+ }>;
51
+ export type ActorClass = z.infer<typeof ActorClassSchema>;
52
+ /**
53
+ * What the resolver concluded. These are the resolver's own four outcomes, kept
54
+ * apart rather than collapsed to null, because "nobody was named" and "somebody
55
+ * was named and we do not know them" call for different follow-up.
56
+ */
57
+ export declare const ActorResolutionSchema: z.ZodEnum<{
58
+ resolved: "resolved";
59
+ unknown_person: "unknown_person";
60
+ ambiguous: "ambiguous";
61
+ no_person_named: "no_person_named";
62
+ }>;
63
+ export type ActorResolution = z.infer<typeof ActorResolutionSchema>;
64
+ /**
65
+ * Who acted — resolved to a person when we know them, and stably identified
66
+ * when we do not.
67
+ *
68
+ * The second half is the part worth reading. A backfill of ~2,400 historical
69
+ * decisions predates the person spine, so a large share of them name somebody
70
+ * the roster has never heard of. Collapsing all of those to one null makes the
71
+ * corpus useless: unresolved actor #1, #2 and #3 become indistinguishable and
72
+ * "who did what, and when" is unanswerable even in principle.
73
+ *
74
+ * So every distinct unresolved label gets `unresolved_actor_id`, derived
75
+ * deterministically from the normalized label (see `unresolvedActorId`). Events
76
+ * from the same label cluster under the same id forever. Later a human asserts
77
+ * "unresolved actor X is Brandon" and every event under that id resolves at
78
+ * once, retroactively, without a single event row being rewritten — the
79
+ * assertion lives in its own append-only alias table.
80
+ *
81
+ * That is not fuzzy matching sneaking back in. The ban is on the machine
82
+ * guessing; a human asserting an identity is the sanctioned escape hatch, and
83
+ * it is the same shape as jarvis_corrections, where the human outranks the
84
+ * pipeline and nothing is edited in place.
85
+ */
86
+ export declare const ActorRefSchema: z.ZodObject<{
87
+ raw_label: z.ZodNullable<z.ZodString>;
88
+ label_kind: z.ZodNullable<z.ZodEnum<{
89
+ email: "email";
90
+ github_login: "github_login";
91
+ linear_email: "linear_email";
92
+ linear_user_id: "linear_user_id";
93
+ reviewer_session: "reviewer_session";
94
+ hook_operator: "hook_operator";
95
+ queue_field: "queue_field";
96
+ brief_variable: "brief_variable";
97
+ }>>;
98
+ actor_class: z.ZodEnum<{
99
+ human: "human";
100
+ agent: "agent";
101
+ }>;
102
+ resolution: z.ZodEnum<{
103
+ resolved: "resolved";
104
+ unknown_person: "unknown_person";
105
+ ambiguous: "ambiguous";
106
+ no_person_named: "no_person_named";
107
+ }>;
108
+ person_id: z.ZodNullable<z.ZodString>;
109
+ unresolved_actor_id: z.ZodNullable<z.ZodString>;
110
+ }, z.core.$strict>;
111
+ export type ActorRef = z.infer<typeof ActorRefSchema>;
112
+ /**
113
+ * The thing decided upon. `repo` is stored canonically as `owner/name` because
114
+ * the source artifacts disagree — the same pull request appears as
115
+ * `bli-cockpit` in the queue and `veetesh-glitch/bli-cockpit` in the verdict.
116
+ * Normalizing is an exact rule, never a prefix guess.
117
+ */
118
+ export declare const DecisionSubjectSchema: z.ZodObject<{
119
+ repo: z.ZodNullable<z.ZodString>;
120
+ branch: z.ZodNullable<z.ZodString>;
121
+ ticket_id: z.ZodNullable<z.ZodString>;
122
+ pull_request_number: z.ZodNullable<z.ZodNumber>;
123
+ head_sha: z.ZodNullable<z.ZodString>;
124
+ }, z.core.$strict>;
125
+ export type DecisionSubject = z.infer<typeof DecisionSubjectSchema>;
126
+ /**
127
+ * Where to go to see the thing itself. Pointers, never content — the prose
128
+ * these point at is exactly what v1 refuses to carry.
129
+ */
130
+ export declare const DecisionEvidencePointerSchema: z.ZodObject<{
131
+ kind: z.ZodEnum<{
132
+ claude_state_path: "claude_state_path";
133
+ github_pull_request: "github_pull_request";
134
+ linear_issue: "linear_issue";
135
+ commit: "commit";
136
+ reviewer_session_id: "reviewer_session_id";
137
+ }>;
138
+ locator: z.ZodString;
139
+ }, z.core.$strict>;
140
+ export type DecisionEvidencePointer = z.infer<typeof DecisionEvidencePointerSchema>;
141
+ export declare const DecisionOutcomeSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
142
+ kind: z.ZodLiteral<"delegation_issued">;
143
+ eta_minutes: z.ZodNullable<z.ZodNumber>;
144
+ model: z.ZodNullable<z.ZodString>;
145
+ auto_merge_authorized: z.ZodBoolean;
146
+ blast_radius_tags: z.ZodArray<z.ZodString>;
147
+ depends_on: z.ZodArray<z.ZodString>;
148
+ acceptance_criteria_count: z.ZodNumber;
149
+ scope_opens_with_why: z.ZodBoolean;
150
+ plan_step: z.ZodEnum<{
151
+ skipped: "skipped";
152
+ required: "required";
153
+ }>;
154
+ }, z.core.$strict>, z.ZodObject<{
155
+ kind: z.ZodLiteral<"review_verdict">;
156
+ verdict: z.ZodEnum<{
157
+ approve: "approve";
158
+ request_changes: "request_changes";
159
+ comment: "comment";
160
+ }>;
161
+ score: z.ZodNullable<z.ZodNumber>;
162
+ must_fix_count: z.ZodNumber;
163
+ should_fix_count: z.ZodNumber;
164
+ gate: z.ZodNullable<z.ZodEnum<{
165
+ CONTINUE: "CONTINUE";
166
+ ITERATE: "ITERATE";
167
+ BLOCK: "BLOCK";
168
+ }>>;
169
+ brief_contract_checked: z.ZodBoolean;
170
+ brief_missed_high_count: z.ZodNumber;
171
+ review_pass: z.ZodNumber;
172
+ reviewer_model: z.ZodNullable<z.ZodString>;
173
+ blast_radius: z.ZodNullable<z.ZodString>;
174
+ auto_merge_eligible: z.ZodNullable<z.ZodBoolean>;
175
+ needs_preview_signoff: z.ZodNullable<z.ZodBoolean>;
176
+ }, z.core.$strict>, z.ZodObject<{
177
+ kind: z.ZodLiteral<"human_approval">;
178
+ approval_kind: z.ZodEnum<{
179
+ explicit_tap: "explicit_tap";
180
+ standing_tap: "standing_tap";
181
+ preview_confirmed: "preview_confirmed";
182
+ merge_authorized: "merge_authorized";
183
+ ruling: "ruling";
184
+ }>;
185
+ granted: z.ZodBoolean;
186
+ scope: z.ZodEnum<{
187
+ this_pr: "this_pr";
188
+ wave: "wave";
189
+ standing: "standing";
190
+ }>;
191
+ }, z.core.$strict>, z.ZodObject<{
192
+ kind: z.ZodLiteral<"intervention">;
193
+ intervention_kind: z.ZodEnum<{
194
+ ruling: "ruling";
195
+ hook_bypass: "hook_bypass";
196
+ send_back: "send_back";
197
+ scope_correction: "scope_correction";
198
+ decision_block: "decision_block";
199
+ re_review_requested: "re_review_requested";
200
+ }>;
201
+ self_reported: z.ZodBoolean;
202
+ hook_name: z.ZodNullable<z.ZodString>;
203
+ blocked_action: z.ZodNullable<z.ZodString>;
204
+ }, z.core.$strict>, z.ZodObject<{
205
+ kind: z.ZodLiteral<"takeover">;
206
+ takeover_kind: z.ZodEnum<{
207
+ human_finished_work: "human_finished_work";
208
+ work_abandoned: "work_abandoned";
209
+ reassigned: "reassigned";
210
+ output_discarded: "output_discarded";
211
+ }>;
212
+ prior_actor: z.ZodObject<{
213
+ raw_label: z.ZodNullable<z.ZodString>;
214
+ label_kind: z.ZodNullable<z.ZodEnum<{
215
+ email: "email";
216
+ github_login: "github_login";
217
+ linear_email: "linear_email";
218
+ linear_user_id: "linear_user_id";
219
+ reviewer_session: "reviewer_session";
220
+ hook_operator: "hook_operator";
221
+ queue_field: "queue_field";
222
+ brief_variable: "brief_variable";
223
+ }>>;
224
+ actor_class: z.ZodEnum<{
225
+ human: "human";
226
+ agent: "agent";
227
+ }>;
228
+ resolution: z.ZodEnum<{
229
+ resolved: "resolved";
230
+ unknown_person: "unknown_person";
231
+ ambiguous: "ambiguous";
232
+ no_person_named: "no_person_named";
233
+ }>;
234
+ person_id: z.ZodNullable<z.ZodString>;
235
+ unresolved_actor_id: z.ZodNullable<z.ZodString>;
236
+ }, z.core.$strict>;
237
+ turns_before_takeover: z.ZodNullable<z.ZodNumber>;
238
+ }, z.core.$strict>, z.ZodObject<{
239
+ kind: z.ZodLiteral<"rule_fired">;
240
+ hook_name: z.ZodString;
241
+ decision: z.ZodEnum<{
242
+ blocked: "blocked";
243
+ allowed: "allowed";
244
+ allowed_bypass: "allowed_bypass";
245
+ warned: "warned";
246
+ }>;
247
+ bypass_token_present: z.ZodBoolean;
248
+ }, z.core.$strict>], "kind">;
249
+ export type DecisionOutcome = z.infer<typeof DecisionOutcomeSchema>;
250
+ export declare const DecisionEventSchema: z.ZodObject<{
251
+ decision_event_id: z.ZodString;
252
+ kind: z.ZodEnum<{
253
+ delegation_issued: "delegation_issued";
254
+ review_verdict: "review_verdict";
255
+ human_approval: "human_approval";
256
+ intervention: "intervention";
257
+ takeover: "takeover";
258
+ rule_fired: "rule_fired";
259
+ }>;
260
+ occurred_at: z.ZodString;
261
+ actor: z.ZodObject<{
262
+ raw_label: z.ZodNullable<z.ZodString>;
263
+ label_kind: z.ZodNullable<z.ZodEnum<{
264
+ email: "email";
265
+ github_login: "github_login";
266
+ linear_email: "linear_email";
267
+ linear_user_id: "linear_user_id";
268
+ reviewer_session: "reviewer_session";
269
+ hook_operator: "hook_operator";
270
+ queue_field: "queue_field";
271
+ brief_variable: "brief_variable";
272
+ }>>;
273
+ actor_class: z.ZodEnum<{
274
+ human: "human";
275
+ agent: "agent";
276
+ }>;
277
+ resolution: z.ZodEnum<{
278
+ resolved: "resolved";
279
+ unknown_person: "unknown_person";
280
+ ambiguous: "ambiguous";
281
+ no_person_named: "no_person_named";
282
+ }>;
283
+ person_id: z.ZodNullable<z.ZodString>;
284
+ unresolved_actor_id: z.ZodNullable<z.ZodString>;
285
+ }, z.core.$strict>;
286
+ subject: z.ZodObject<{
287
+ repo: z.ZodNullable<z.ZodString>;
288
+ branch: z.ZodNullable<z.ZodString>;
289
+ ticket_id: z.ZodNullable<z.ZodString>;
290
+ pull_request_number: z.ZodNullable<z.ZodNumber>;
291
+ head_sha: z.ZodNullable<z.ZodString>;
292
+ }, z.core.$strict>;
293
+ outcome: z.ZodDiscriminatedUnion<[z.ZodObject<{
294
+ kind: z.ZodLiteral<"delegation_issued">;
295
+ eta_minutes: z.ZodNullable<z.ZodNumber>;
296
+ model: z.ZodNullable<z.ZodString>;
297
+ auto_merge_authorized: z.ZodBoolean;
298
+ blast_radius_tags: z.ZodArray<z.ZodString>;
299
+ depends_on: z.ZodArray<z.ZodString>;
300
+ acceptance_criteria_count: z.ZodNumber;
301
+ scope_opens_with_why: z.ZodBoolean;
302
+ plan_step: z.ZodEnum<{
303
+ skipped: "skipped";
304
+ required: "required";
305
+ }>;
306
+ }, z.core.$strict>, z.ZodObject<{
307
+ kind: z.ZodLiteral<"review_verdict">;
308
+ verdict: z.ZodEnum<{
309
+ approve: "approve";
310
+ request_changes: "request_changes";
311
+ comment: "comment";
312
+ }>;
313
+ score: z.ZodNullable<z.ZodNumber>;
314
+ must_fix_count: z.ZodNumber;
315
+ should_fix_count: z.ZodNumber;
316
+ gate: z.ZodNullable<z.ZodEnum<{
317
+ CONTINUE: "CONTINUE";
318
+ ITERATE: "ITERATE";
319
+ BLOCK: "BLOCK";
320
+ }>>;
321
+ brief_contract_checked: z.ZodBoolean;
322
+ brief_missed_high_count: z.ZodNumber;
323
+ review_pass: z.ZodNumber;
324
+ reviewer_model: z.ZodNullable<z.ZodString>;
325
+ blast_radius: z.ZodNullable<z.ZodString>;
326
+ auto_merge_eligible: z.ZodNullable<z.ZodBoolean>;
327
+ needs_preview_signoff: z.ZodNullable<z.ZodBoolean>;
328
+ }, z.core.$strict>, z.ZodObject<{
329
+ kind: z.ZodLiteral<"human_approval">;
330
+ approval_kind: z.ZodEnum<{
331
+ explicit_tap: "explicit_tap";
332
+ standing_tap: "standing_tap";
333
+ preview_confirmed: "preview_confirmed";
334
+ merge_authorized: "merge_authorized";
335
+ ruling: "ruling";
336
+ }>;
337
+ granted: z.ZodBoolean;
338
+ scope: z.ZodEnum<{
339
+ this_pr: "this_pr";
340
+ wave: "wave";
341
+ standing: "standing";
342
+ }>;
343
+ }, z.core.$strict>, z.ZodObject<{
344
+ kind: z.ZodLiteral<"intervention">;
345
+ intervention_kind: z.ZodEnum<{
346
+ ruling: "ruling";
347
+ hook_bypass: "hook_bypass";
348
+ send_back: "send_back";
349
+ scope_correction: "scope_correction";
350
+ decision_block: "decision_block";
351
+ re_review_requested: "re_review_requested";
352
+ }>;
353
+ self_reported: z.ZodBoolean;
354
+ hook_name: z.ZodNullable<z.ZodString>;
355
+ blocked_action: z.ZodNullable<z.ZodString>;
356
+ }, z.core.$strict>, z.ZodObject<{
357
+ kind: z.ZodLiteral<"takeover">;
358
+ takeover_kind: z.ZodEnum<{
359
+ human_finished_work: "human_finished_work";
360
+ work_abandoned: "work_abandoned";
361
+ reassigned: "reassigned";
362
+ output_discarded: "output_discarded";
363
+ }>;
364
+ prior_actor: z.ZodObject<{
365
+ raw_label: z.ZodNullable<z.ZodString>;
366
+ label_kind: z.ZodNullable<z.ZodEnum<{
367
+ email: "email";
368
+ github_login: "github_login";
369
+ linear_email: "linear_email";
370
+ linear_user_id: "linear_user_id";
371
+ reviewer_session: "reviewer_session";
372
+ hook_operator: "hook_operator";
373
+ queue_field: "queue_field";
374
+ brief_variable: "brief_variable";
375
+ }>>;
376
+ actor_class: z.ZodEnum<{
377
+ human: "human";
378
+ agent: "agent";
379
+ }>;
380
+ resolution: z.ZodEnum<{
381
+ resolved: "resolved";
382
+ unknown_person: "unknown_person";
383
+ ambiguous: "ambiguous";
384
+ no_person_named: "no_person_named";
385
+ }>;
386
+ person_id: z.ZodNullable<z.ZodString>;
387
+ unresolved_actor_id: z.ZodNullable<z.ZodString>;
388
+ }, z.core.$strict>;
389
+ turns_before_takeover: z.ZodNullable<z.ZodNumber>;
390
+ }, z.core.$strict>, z.ZodObject<{
391
+ kind: z.ZodLiteral<"rule_fired">;
392
+ hook_name: z.ZodString;
393
+ decision: z.ZodEnum<{
394
+ blocked: "blocked";
395
+ allowed: "allowed";
396
+ allowed_bypass: "allowed_bypass";
397
+ warned: "warned";
398
+ }>;
399
+ bypass_token_present: z.ZodBoolean;
400
+ }, z.core.$strict>], "kind">;
401
+ capture_source: z.ZodEnum<{
402
+ claude_state_verdict_json: "claude_state_verdict_json";
403
+ claude_state_queue_jsonl: "claude_state_queue_jsonl";
404
+ claude_state_worker_brief: "claude_state_worker_brief";
405
+ claude_state_retro_jsonl: "claude_state_retro_jsonl";
406
+ claude_state_bypass_log: "claude_state_bypass_log";
407
+ claude_state_merge_log: "claude_state_merge_log";
408
+ hook_runtime: "hook_runtime";
409
+ }>;
410
+ source_artifact_hash_sha256: z.ZodNullable<z.ZodString>;
411
+ source_line_number: z.ZodNullable<z.ZodNumber>;
412
+ rule_id: z.ZodNullable<z.ZodString>;
413
+ rulebook_version: z.ZodNullable<z.ZodString>;
414
+ host: z.ZodNullable<z.ZodString>;
415
+ evidence_pointers: z.ZodArray<z.ZodObject<{
416
+ kind: z.ZodEnum<{
417
+ claude_state_path: "claude_state_path";
418
+ github_pull_request: "github_pull_request";
419
+ linear_issue: "linear_issue";
420
+ commit: "commit";
421
+ reviewer_session_id: "reviewer_session_id";
422
+ }>;
423
+ locator: z.ZodString;
424
+ }, z.core.$strict>>;
425
+ attributes: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>]>>;
426
+ privacy_classification: z.ZodLiteral<"metadata">;
427
+ }, z.core.$strict>;
428
+ export type DecisionEvent = z.infer<typeof DecisionEventSchema>;
429
+ export declare const DecisionEventEnvelopeSchema: z.ZodObject<{
430
+ envelope_version: z.ZodLiteral<"decision-event.v1">;
431
+ writer: z.ZodString;
432
+ writer_version: z.ZodString;
433
+ events: z.ZodArray<z.ZodObject<{
434
+ decision_event_id: z.ZodString;
435
+ kind: z.ZodEnum<{
436
+ delegation_issued: "delegation_issued";
437
+ review_verdict: "review_verdict";
438
+ human_approval: "human_approval";
439
+ intervention: "intervention";
440
+ takeover: "takeover";
441
+ rule_fired: "rule_fired";
442
+ }>;
443
+ occurred_at: z.ZodString;
444
+ actor: z.ZodObject<{
445
+ raw_label: z.ZodNullable<z.ZodString>;
446
+ label_kind: z.ZodNullable<z.ZodEnum<{
447
+ email: "email";
448
+ github_login: "github_login";
449
+ linear_email: "linear_email";
450
+ linear_user_id: "linear_user_id";
451
+ reviewer_session: "reviewer_session";
452
+ hook_operator: "hook_operator";
453
+ queue_field: "queue_field";
454
+ brief_variable: "brief_variable";
455
+ }>>;
456
+ actor_class: z.ZodEnum<{
457
+ human: "human";
458
+ agent: "agent";
459
+ }>;
460
+ resolution: z.ZodEnum<{
461
+ resolved: "resolved";
462
+ unknown_person: "unknown_person";
463
+ ambiguous: "ambiguous";
464
+ no_person_named: "no_person_named";
465
+ }>;
466
+ person_id: z.ZodNullable<z.ZodString>;
467
+ unresolved_actor_id: z.ZodNullable<z.ZodString>;
468
+ }, z.core.$strict>;
469
+ subject: z.ZodObject<{
470
+ repo: z.ZodNullable<z.ZodString>;
471
+ branch: z.ZodNullable<z.ZodString>;
472
+ ticket_id: z.ZodNullable<z.ZodString>;
473
+ pull_request_number: z.ZodNullable<z.ZodNumber>;
474
+ head_sha: z.ZodNullable<z.ZodString>;
475
+ }, z.core.$strict>;
476
+ outcome: z.ZodDiscriminatedUnion<[z.ZodObject<{
477
+ kind: z.ZodLiteral<"delegation_issued">;
478
+ eta_minutes: z.ZodNullable<z.ZodNumber>;
479
+ model: z.ZodNullable<z.ZodString>;
480
+ auto_merge_authorized: z.ZodBoolean;
481
+ blast_radius_tags: z.ZodArray<z.ZodString>;
482
+ depends_on: z.ZodArray<z.ZodString>;
483
+ acceptance_criteria_count: z.ZodNumber;
484
+ scope_opens_with_why: z.ZodBoolean;
485
+ plan_step: z.ZodEnum<{
486
+ skipped: "skipped";
487
+ required: "required";
488
+ }>;
489
+ }, z.core.$strict>, z.ZodObject<{
490
+ kind: z.ZodLiteral<"review_verdict">;
491
+ verdict: z.ZodEnum<{
492
+ approve: "approve";
493
+ request_changes: "request_changes";
494
+ comment: "comment";
495
+ }>;
496
+ score: z.ZodNullable<z.ZodNumber>;
497
+ must_fix_count: z.ZodNumber;
498
+ should_fix_count: z.ZodNumber;
499
+ gate: z.ZodNullable<z.ZodEnum<{
500
+ CONTINUE: "CONTINUE";
501
+ ITERATE: "ITERATE";
502
+ BLOCK: "BLOCK";
503
+ }>>;
504
+ brief_contract_checked: z.ZodBoolean;
505
+ brief_missed_high_count: z.ZodNumber;
506
+ review_pass: z.ZodNumber;
507
+ reviewer_model: z.ZodNullable<z.ZodString>;
508
+ blast_radius: z.ZodNullable<z.ZodString>;
509
+ auto_merge_eligible: z.ZodNullable<z.ZodBoolean>;
510
+ needs_preview_signoff: z.ZodNullable<z.ZodBoolean>;
511
+ }, z.core.$strict>, z.ZodObject<{
512
+ kind: z.ZodLiteral<"human_approval">;
513
+ approval_kind: z.ZodEnum<{
514
+ explicit_tap: "explicit_tap";
515
+ standing_tap: "standing_tap";
516
+ preview_confirmed: "preview_confirmed";
517
+ merge_authorized: "merge_authorized";
518
+ ruling: "ruling";
519
+ }>;
520
+ granted: z.ZodBoolean;
521
+ scope: z.ZodEnum<{
522
+ this_pr: "this_pr";
523
+ wave: "wave";
524
+ standing: "standing";
525
+ }>;
526
+ }, z.core.$strict>, z.ZodObject<{
527
+ kind: z.ZodLiteral<"intervention">;
528
+ intervention_kind: z.ZodEnum<{
529
+ ruling: "ruling";
530
+ hook_bypass: "hook_bypass";
531
+ send_back: "send_back";
532
+ scope_correction: "scope_correction";
533
+ decision_block: "decision_block";
534
+ re_review_requested: "re_review_requested";
535
+ }>;
536
+ self_reported: z.ZodBoolean;
537
+ hook_name: z.ZodNullable<z.ZodString>;
538
+ blocked_action: z.ZodNullable<z.ZodString>;
539
+ }, z.core.$strict>, z.ZodObject<{
540
+ kind: z.ZodLiteral<"takeover">;
541
+ takeover_kind: z.ZodEnum<{
542
+ human_finished_work: "human_finished_work";
543
+ work_abandoned: "work_abandoned";
544
+ reassigned: "reassigned";
545
+ output_discarded: "output_discarded";
546
+ }>;
547
+ prior_actor: z.ZodObject<{
548
+ raw_label: z.ZodNullable<z.ZodString>;
549
+ label_kind: z.ZodNullable<z.ZodEnum<{
550
+ email: "email";
551
+ github_login: "github_login";
552
+ linear_email: "linear_email";
553
+ linear_user_id: "linear_user_id";
554
+ reviewer_session: "reviewer_session";
555
+ hook_operator: "hook_operator";
556
+ queue_field: "queue_field";
557
+ brief_variable: "brief_variable";
558
+ }>>;
559
+ actor_class: z.ZodEnum<{
560
+ human: "human";
561
+ agent: "agent";
562
+ }>;
563
+ resolution: z.ZodEnum<{
564
+ resolved: "resolved";
565
+ unknown_person: "unknown_person";
566
+ ambiguous: "ambiguous";
567
+ no_person_named: "no_person_named";
568
+ }>;
569
+ person_id: z.ZodNullable<z.ZodString>;
570
+ unresolved_actor_id: z.ZodNullable<z.ZodString>;
571
+ }, z.core.$strict>;
572
+ turns_before_takeover: z.ZodNullable<z.ZodNumber>;
573
+ }, z.core.$strict>, z.ZodObject<{
574
+ kind: z.ZodLiteral<"rule_fired">;
575
+ hook_name: z.ZodString;
576
+ decision: z.ZodEnum<{
577
+ blocked: "blocked";
578
+ allowed: "allowed";
579
+ allowed_bypass: "allowed_bypass";
580
+ warned: "warned";
581
+ }>;
582
+ bypass_token_present: z.ZodBoolean;
583
+ }, z.core.$strict>], "kind">;
584
+ capture_source: z.ZodEnum<{
585
+ claude_state_verdict_json: "claude_state_verdict_json";
586
+ claude_state_queue_jsonl: "claude_state_queue_jsonl";
587
+ claude_state_worker_brief: "claude_state_worker_brief";
588
+ claude_state_retro_jsonl: "claude_state_retro_jsonl";
589
+ claude_state_bypass_log: "claude_state_bypass_log";
590
+ claude_state_merge_log: "claude_state_merge_log";
591
+ hook_runtime: "hook_runtime";
592
+ }>;
593
+ source_artifact_hash_sha256: z.ZodNullable<z.ZodString>;
594
+ source_line_number: z.ZodNullable<z.ZodNumber>;
595
+ rule_id: z.ZodNullable<z.ZodString>;
596
+ rulebook_version: z.ZodNullable<z.ZodString>;
597
+ host: z.ZodNullable<z.ZodString>;
598
+ evidence_pointers: z.ZodArray<z.ZodObject<{
599
+ kind: z.ZodEnum<{
600
+ claude_state_path: "claude_state_path";
601
+ github_pull_request: "github_pull_request";
602
+ linear_issue: "linear_issue";
603
+ commit: "commit";
604
+ reviewer_session_id: "reviewer_session_id";
605
+ }>;
606
+ locator: z.ZodString;
607
+ }, z.core.$strict>>;
608
+ attributes: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodNull]>>]>>;
609
+ privacy_classification: z.ZodLiteral<"metadata">;
610
+ }, z.core.$strict>>;
611
+ }, z.core.$strict>;
612
+ export type DecisionEventEnvelope = z.infer<typeof DecisionEventEnvelopeSchema>;
@@ -0,0 +1,406 @@
1
+ // BLI-2425 / Tower Wave 1: what a person committed to, as a record.
2
+ //
3
+ // A decision event is one durable record that a person committed to something:
4
+ // handed work to someone, judged work, approved a merge, stepped in, took work
5
+ // over, or had a rule fire on them. It is not a session, not a turn, and not a
6
+ // diff — the ambient path already owns those, and this vocabulary deliberately
7
+ // does not touch it.
8
+ //
9
+ // Three properties drive every field below.
10
+ //
11
+ // 1. The actor is a person, resolved through the one resolver. No fuzzy
12
+ // matching, ever. But an unresolved actor is not a dead end either: it gets
13
+ // a stable id so that "who did what, and when" stays answerable before
14
+ // anybody knows the name, and so that one later human assertion resolves
15
+ // every event that actor ever produced at once. See ActorRefSchema.
16
+ //
17
+ // 2. The event carries pointers, never content. Verdict prose and brief bodies
18
+ // name client repos and quote people. v1 carries enums, counts, ids, hashes
19
+ // and evidence pointers. Nothing else. `privacy_classification` is pinned to
20
+ // the literal "metadata" rather than reusing the full enum, so the type
21
+ // system refuses a decision event that could legally carry raw evidence.
22
+ //
23
+ // 3. There is a rule slot even though no rule IDs exist yet. Nothing anywhere
24
+ // records which rules were in force when something happened. The slot ships
25
+ // nullable in v1 so Wave 2 fills it by backfill instead of by migration.
26
+ //
27
+ // This vocabulary is disjoint from KNOWN_EVENT_TYPES and payloadSchemas. No
28
+ // kind here reuses a legacy event name and nothing here lands on
29
+ // /api/events/emit. That separation is the point: the known three-way
30
+ // event-vocabulary drift is not a dependency of this work.
31
+ import { z } from "zod";
32
+ import { IsoDateTimeSchema, JsonAttributeRecordSchema, NonEmptyStringSchema, Sha256Schema, VersionStringSchema, } from "./common.js";
33
+ export const DECISION_EVENT_ENVELOPE_VERSION = "decision-event.v1";
34
+ /** A git commit sha. Sha256Schema is 64 hex and does not fit a 40-hex commit. */
35
+ export const CommitShaSchema = z.string().regex(/^[a-f0-9]{7,40}$/);
36
+ // ---------------------------------------------------------------------------
37
+ // Kinds
38
+ // ---------------------------------------------------------------------------
39
+ export const DecisionEventKindSchema = z.enum([
40
+ "delegation_issued",
41
+ "review_verdict",
42
+ "human_approval",
43
+ "intervention",
44
+ "takeover",
45
+ "rule_fired",
46
+ ]);
47
+ /**
48
+ * Which artifact this event was read out of.
49
+ *
50
+ * Deliberately its own enum rather than a member added to CaptureSourceSchema.
51
+ * That enum describes what a collector watched on a coding machine; these are
52
+ * files a manager's harness wrote as a side effect of managing. Sharing the
53
+ * enum would imply a shared envelope, and the whole reason this path exists is
54
+ * that decision records have no session, no work context and no capture
55
+ * provenance to fill.
56
+ */
57
+ export const DecisionCaptureSourceSchema = z.enum([
58
+ "claude_state_verdict_json",
59
+ "claude_state_queue_jsonl",
60
+ "claude_state_worker_brief",
61
+ "claude_state_retro_jsonl",
62
+ "claude_state_bypass_log",
63
+ "claude_state_merge_log",
64
+ "hook_runtime",
65
+ ]);
66
+ // ---------------------------------------------------------------------------
67
+ // Who
68
+ // ---------------------------------------------------------------------------
69
+ /** Which field of the source artifact produced this label. */
70
+ export const ActorLabelKindSchema = z.enum([
71
+ "email",
72
+ "github_login",
73
+ "linear_email",
74
+ "linear_user_id",
75
+ "reviewer_session",
76
+ "hook_operator",
77
+ "queue_field",
78
+ "brief_variable",
79
+ ]);
80
+ /** Whether the thing that acted was a person or something a person ran. */
81
+ export const ActorClassSchema = z.enum(["human", "agent"]);
82
+ /**
83
+ * What the resolver concluded. These are the resolver's own four outcomes, kept
84
+ * apart rather than collapsed to null, because "nobody was named" and "somebody
85
+ * was named and we do not know them" call for different follow-up.
86
+ */
87
+ export const ActorResolutionSchema = z.enum([
88
+ "resolved",
89
+ "unknown_person",
90
+ "ambiguous",
91
+ "no_person_named",
92
+ ]);
93
+ /**
94
+ * Who acted — resolved to a person when we know them, and stably identified
95
+ * when we do not.
96
+ *
97
+ * The second half is the part worth reading. A backfill of ~2,400 historical
98
+ * decisions predates the person spine, so a large share of them name somebody
99
+ * the roster has never heard of. Collapsing all of those to one null makes the
100
+ * corpus useless: unresolved actor #1, #2 and #3 become indistinguishable and
101
+ * "who did what, and when" is unanswerable even in principle.
102
+ *
103
+ * So every distinct unresolved label gets `unresolved_actor_id`, derived
104
+ * deterministically from the normalized label (see `unresolvedActorId`). Events
105
+ * from the same label cluster under the same id forever. Later a human asserts
106
+ * "unresolved actor X is Brandon" and every event under that id resolves at
107
+ * once, retroactively, without a single event row being rewritten — the
108
+ * assertion lives in its own append-only alias table.
109
+ *
110
+ * That is not fuzzy matching sneaking back in. The ban is on the machine
111
+ * guessing; a human asserting an identity is the sanctioned escape hatch, and
112
+ * it is the same shape as jarvis_corrections, where the human outranks the
113
+ * pipeline and nothing is edited in place.
114
+ */
115
+ export const ActorRefSchema = z
116
+ .object({
117
+ /** Exactly what the artifact said, before any normalization. Kept forever. */
118
+ raw_label: NonEmptyStringSchema.max(300).nullable(),
119
+ label_kind: ActorLabelKindSchema.nullable(),
120
+ actor_class: ActorClassSchema,
121
+ resolution: ActorResolutionSchema,
122
+ /** The roster person, when the resolver found exactly one. */
123
+ person_id: NonEmptyStringSchema.nullable(),
124
+ /**
125
+ * The stable identity of an actor we cannot name yet. Set whenever a real
126
+ * label failed to resolve; null when the resolver matched, and null when
127
+ * the artifact named nobody at all (there is no actor to be stable about).
128
+ */
129
+ unresolved_actor_id: NonEmptyStringSchema.nullable(),
130
+ })
131
+ .strict()
132
+ .superRefine((actor, context) => {
133
+ if (actor.resolution === "resolved") {
134
+ if (!actor.person_id) {
135
+ context.addIssue({
136
+ code: "custom",
137
+ path: ["person_id"],
138
+ message: "A resolved actor must carry the person it resolved to.",
139
+ });
140
+ }
141
+ if (actor.unresolved_actor_id) {
142
+ context.addIssue({
143
+ code: "custom",
144
+ path: ["unresolved_actor_id"],
145
+ message: "A resolved actor has no unresolved id. Carrying both invites two answers to one question.",
146
+ });
147
+ }
148
+ return;
149
+ }
150
+ if (actor.person_id) {
151
+ context.addIssue({
152
+ code: "custom",
153
+ path: ["person_id"],
154
+ message: "Only a resolved actor carries a person. An unresolved actor that names one is a guess.",
155
+ });
156
+ }
157
+ if (actor.resolution === "no_person_named") {
158
+ if (actor.raw_label) {
159
+ context.addIssue({
160
+ code: "custom",
161
+ path: ["raw_label"],
162
+ message: "no_person_named means the artifact named nobody. A label present here is a different outcome.",
163
+ });
164
+ }
165
+ if (actor.unresolved_actor_id) {
166
+ context.addIssue({
167
+ code: "custom",
168
+ path: ["unresolved_actor_id"],
169
+ message: "Nobody was named, so there is no actor to identify stably.",
170
+ });
171
+ }
172
+ return;
173
+ }
174
+ // unknown_person and ambiguous: a real label that we could not turn into
175
+ // exactly one person. Both must be stably identified, or the backfill loses
176
+ // the ability to tell two strangers apart.
177
+ if (!actor.raw_label) {
178
+ context.addIssue({
179
+ code: "custom",
180
+ path: ["raw_label"],
181
+ message: "An unresolved actor must keep the label it failed to resolve, or nobody can ever name it.",
182
+ });
183
+ }
184
+ if (!actor.unresolved_actor_id) {
185
+ context.addIssue({
186
+ code: "custom",
187
+ path: ["unresolved_actor_id"],
188
+ message: "An unresolved actor must carry a stable id so its events cluster and can be named later.",
189
+ });
190
+ }
191
+ });
192
+ // ---------------------------------------------------------------------------
193
+ // What it was about
194
+ // ---------------------------------------------------------------------------
195
+ /**
196
+ * The thing decided upon. `repo` is stored canonically as `owner/name` because
197
+ * the source artifacts disagree — the same pull request appears as
198
+ * `bli-cockpit` in the queue and `veetesh-glitch/bli-cockpit` in the verdict.
199
+ * Normalizing is an exact rule, never a prefix guess.
200
+ */
201
+ export const DecisionSubjectSchema = z
202
+ .object({
203
+ repo: NonEmptyStringSchema.max(200).nullable(),
204
+ branch: NonEmptyStringSchema.max(300).nullable(),
205
+ ticket_id: NonEmptyStringSchema.max(40).nullable(),
206
+ pull_request_number: z.number().int().positive().nullable(),
207
+ head_sha: CommitShaSchema.nullable(),
208
+ })
209
+ .strict();
210
+ /**
211
+ * Where to go to see the thing itself. Pointers, never content — the prose
212
+ * these point at is exactly what v1 refuses to carry.
213
+ */
214
+ export const DecisionEvidencePointerSchema = z
215
+ .object({
216
+ kind: z.enum([
217
+ "claude_state_path",
218
+ "github_pull_request",
219
+ "linear_issue",
220
+ "commit",
221
+ "reviewer_session_id",
222
+ ]),
223
+ locator: NonEmptyStringSchema.max(500),
224
+ })
225
+ .strict();
226
+ // ---------------------------------------------------------------------------
227
+ // Per-kind outcomes
228
+ // ---------------------------------------------------------------------------
229
+ // Each is deliberately small: counts and enums, no narrative. Where the source
230
+ // artifact holds a list of prose items, only its length survives.
231
+ const DelegationIssuedOutcomeSchema = z
232
+ .object({
233
+ kind: z.literal("delegation_issued"),
234
+ eta_minutes: z.number().int().nonnegative().nullable(),
235
+ model: NonEmptyStringSchema.max(120).nullable(),
236
+ auto_merge_authorized: z.boolean(),
237
+ blast_radius_tags: z.array(NonEmptyStringSchema.max(60)).max(40),
238
+ depends_on: z.array(NonEmptyStringSchema.max(40)).max(40),
239
+ acceptance_criteria_count: z.number().int().nonnegative(),
240
+ scope_opens_with_why: z.boolean(),
241
+ plan_step: z.enum(["required", "skipped"]),
242
+ })
243
+ .strict();
244
+ const ReviewVerdictOutcomeSchema = z
245
+ .object({
246
+ kind: z.literal("review_verdict"),
247
+ verdict: z.enum(["approve", "request_changes", "comment"]),
248
+ score: z.number().min(0).max(10).nullable(),
249
+ must_fix_count: z.number().int().nonnegative(),
250
+ should_fix_count: z.number().int().nonnegative(),
251
+ gate: z.enum(["CONTINUE", "ITERATE", "BLOCK"]).nullable(),
252
+ brief_contract_checked: z.boolean(),
253
+ brief_missed_high_count: z.number().int().nonnegative(),
254
+ review_pass: z.number().int().positive(),
255
+ reviewer_model: NonEmptyStringSchema.max(120).nullable(),
256
+ blast_radius: NonEmptyStringSchema.max(60).nullable(),
257
+ auto_merge_eligible: z.boolean().nullable(),
258
+ needs_preview_signoff: z.boolean().nullable(),
259
+ })
260
+ .strict();
261
+ const HumanApprovalOutcomeSchema = z
262
+ .object({
263
+ kind: z.literal("human_approval"),
264
+ approval_kind: z.enum([
265
+ "explicit_tap",
266
+ "standing_tap",
267
+ "preview_confirmed",
268
+ "merge_authorized",
269
+ "ruling",
270
+ ]),
271
+ granted: z.boolean(),
272
+ scope: z.enum(["this_pr", "wave", "standing"]),
273
+ })
274
+ .strict();
275
+ const InterventionOutcomeSchema = z
276
+ .object({
277
+ kind: z.literal("intervention"),
278
+ intervention_kind: z.enum([
279
+ "hook_bypass",
280
+ "send_back",
281
+ "scope_correction",
282
+ "decision_block",
283
+ "ruling",
284
+ "re_review_requested",
285
+ ]),
286
+ self_reported: z.boolean(),
287
+ hook_name: NonEmptyStringSchema.max(120).nullable(),
288
+ blocked_action: NonEmptyStringSchema.max(120).nullable(),
289
+ })
290
+ .strict();
291
+ /**
292
+ * Nothing writes this yet — failure handling is modeled nowhere, which the
293
+ * audit called the loudest misdelegation signal being invisible. It is
294
+ * specified now anyway, because adding a kind later is additive within v1 while
295
+ * reshaping the envelope later is not.
296
+ */
297
+ const TakeoverOutcomeSchema = z
298
+ .object({
299
+ kind: z.literal("takeover"),
300
+ takeover_kind: z.enum([
301
+ "human_finished_work",
302
+ "work_abandoned",
303
+ "reassigned",
304
+ "output_discarded",
305
+ ]),
306
+ prior_actor: ActorRefSchema,
307
+ turns_before_takeover: z.number().int().nonnegative().nullable(),
308
+ })
309
+ .strict();
310
+ const RuleFiredOutcomeSchema = z
311
+ .object({
312
+ kind: z.literal("rule_fired"),
313
+ hook_name: NonEmptyStringSchema.max(120),
314
+ decision: z.enum(["allowed", "allowed_bypass", "blocked", "warned"]),
315
+ bypass_token_present: z.boolean(),
316
+ })
317
+ .strict();
318
+ export const DecisionOutcomeSchema = z.discriminatedUnion("kind", [
319
+ DelegationIssuedOutcomeSchema,
320
+ ReviewVerdictOutcomeSchema,
321
+ HumanApprovalOutcomeSchema,
322
+ InterventionOutcomeSchema,
323
+ TakeoverOutcomeSchema,
324
+ RuleFiredOutcomeSchema,
325
+ ]);
326
+ // ---------------------------------------------------------------------------
327
+ // The event
328
+ // ---------------------------------------------------------------------------
329
+ export const DecisionEventSchema = z
330
+ .object({
331
+ /**
332
+ * Deterministic, so re-running the backfill is idempotent and an edited
333
+ * record produces a new id rather than mutating an old one. Derived from the
334
+ * record's own bytes, never from the file around them — see
335
+ * `decisionEventId`.
336
+ */
337
+ decision_event_id: NonEmptyStringSchema.max(64),
338
+ kind: DecisionEventKindSchema,
339
+ occurred_at: IsoDateTimeSchema,
340
+ actor: ActorRefSchema,
341
+ subject: DecisionSubjectSchema,
342
+ outcome: DecisionOutcomeSchema,
343
+ capture_source: DecisionCaptureSourceSchema,
344
+ /**
345
+ * Which snapshot of which file this was read out of, and where in it. Both
346
+ * are provenance, not identity: a re-import of a ledger that has grown since
347
+ * refreshes them on the existing row. Identity comes from the record's own
348
+ * bytes instead, which is what lets that re-import update rather than
349
+ * duplicate — see `decisionEventId`.
350
+ */
351
+ source_artifact_hash_sha256: Sha256Schema.nullable(),
352
+ source_line_number: z.number().int().nonnegative().nullable(),
353
+ /**
354
+ * Which rule this event was governed by, once rules have IDs. Nullable in
355
+ * v1 on purpose: the slot exists so Wave 2 backfills into it rather than
356
+ * migrating for it.
357
+ */
358
+ rule_id: NonEmptyStringSchema.max(40).nullable(),
359
+ rulebook_version: VersionStringSchema.nullable(),
360
+ /**
361
+ * Which machine the decision was made on, straight from the ledger line.
362
+ *
363
+ * A column rather than an `attributes` key, and deliberately beside
364
+ * `rulebook_version`: both are provenance about the *decision* rather than
365
+ * about the actor, and "did the laptop behave differently from the desktop"
366
+ * is a group-by question. A jsonb key would answer it by digging.
367
+ *
368
+ * Nullable because both ledgers only started stamping `host` on 2026-08-04
369
+ * (BLI-2435). Every line before that legitimately has none, so absence is
370
+ * history rather than an error, and a record without one must ingest
371
+ * cleanly.
372
+ */
373
+ host: NonEmptyStringSchema.max(253).nullable(),
374
+ evidence_pointers: z.array(DecisionEvidencePointerSchema).max(20),
375
+ /**
376
+ * Small, flat, non-narrative extras. Enforced as scalar-or-scalar-array by
377
+ * the shared record schema, which is what keeps prose from arriving here
378
+ * under a different name.
379
+ */
380
+ attributes: JsonAttributeRecordSchema,
381
+ /**
382
+ * Pinned to the literal rather than the full enum. A decision event that
383
+ * could legally carry raw evidence is one that will eventually carry
384
+ * verdict prose, so the type system refuses it outright.
385
+ */
386
+ privacy_classification: z.literal("metadata"),
387
+ })
388
+ .strict()
389
+ .superRefine((event, context) => {
390
+ if (event.outcome.kind !== event.kind) {
391
+ context.addIssue({
392
+ code: "custom",
393
+ path: ["outcome", "kind"],
394
+ message: `Outcome is ${event.outcome.kind} but the event says ${event.kind}.`,
395
+ });
396
+ }
397
+ });
398
+ export const DecisionEventEnvelopeSchema = z
399
+ .object({
400
+ envelope_version: z.literal(DECISION_EVENT_ENVELOPE_VERSION),
401
+ /** Who is uploading, e.g. the hook, or the one-off backfill run. */
402
+ writer: NonEmptyStringSchema.max(120),
403
+ writer_version: VersionStringSchema,
404
+ events: z.array(DecisionEventSchema).min(1).max(500),
405
+ })
406
+ .strict();
@@ -389,9 +389,9 @@ export type RawEvidenceUploadCommitResponse = z.infer<typeof RawEvidenceUploadCo
389
389
  export declare const CODEX_SESSION_ATTRIBUTION_STATES: readonly ["attributed", "attributed_fallback", "ambiguous", "unattributed", "skipped"];
390
390
  export declare const CodexSessionAttributionStateSchema: z.ZodEnum<{
391
391
  skipped: "skipped";
392
+ ambiguous: "ambiguous";
392
393
  attributed: "attributed";
393
394
  attributed_fallback: "attributed_fallback";
394
- ambiguous: "ambiguous";
395
395
  unattributed: "unattributed";
396
396
  }>;
397
397
  export type CodexSessionAttributionState = z.infer<typeof CodexSessionAttributionStateSchema>;
@@ -423,9 +423,9 @@ export declare const CodexSessionAttributionSchema: z.ZodObject<{
423
423
  observed_at: z.ZodString;
424
424
  attribution_state: z.ZodEnum<{
425
425
  skipped: "skipped";
426
+ ambiguous: "ambiguous";
426
427
  attributed: "attributed";
427
428
  attributed_fallback: "attributed_fallback";
428
- ambiguous: "ambiguous";
429
429
  unattributed: "unattributed";
430
430
  }>;
431
431
  attribution_reason: z.ZodString;
@@ -491,9 +491,9 @@ export declare const CodexSessionAttributionReportRequestSchema: z.ZodObject<{
491
491
  observed_at: z.ZodString;
492
492
  attribution_state: z.ZodEnum<{
493
493
  skipped: "skipped";
494
+ ambiguous: "ambiguous";
494
495
  attributed: "attributed";
495
496
  attributed_fallback: "attributed_fallback";
496
- ambiguous: "ambiguous";
497
497
  unattributed: "unattributed";
498
498
  }>;
499
499
  attribution_reason: z.ZodString;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from "./agent-artifacts.js";
2
2
  export * from "./common.js";
3
+ export * from "./decision-event.js";
4
+ export * from "./decision-event-id.js";
3
5
  export * from "./evidence-completeness.js";
4
6
  export * from "./evidence-upload.js";
5
7
  export * from "./ingest-dto.js";
package/dist/index.js CHANGED
@@ -1,5 +1,7 @@
1
1
  export * from "./agent-artifacts.js";
2
2
  export * from "./common.js";
3
+ export * from "./decision-event.js";
4
+ export * from "./decision-event-id.js";
3
5
  export * from "./evidence-completeness.js";
4
6
  export * from "./evidence-upload.js";
5
7
  export * from "./ingest-dto.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/telemetry-core",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",