@boxxy/pepys 0.0.1 → 0.1.4

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.
Files changed (63) hide show
  1. package/README.md +42 -5
  2. package/dist/boxxy-source.d.ts +636 -0
  3. package/dist/boxxy-work-plugin.d.ts +1 -0
  4. package/dist/build-boxxy-plugin.d.ts +181 -0
  5. package/dist/consumers/apply-scope.d.ts +5 -0
  6. package/dist/create-pepys-fact-sink.d.ts +9 -0
  7. package/dist/create-pepys-reader.d.ts +9 -0
  8. package/dist/generated/boxxy-work-boundary.d.ts +566 -0
  9. package/dist/index.d.ts +5 -0
  10. package/dist/queries/build-cost-to-land-query.d.ts +156 -0
  11. package/dist/queries/build-cost-to-land.d.ts +12 -0
  12. package/dist/queries/build-stage-time-query.d.ts +29 -0
  13. package/dist/queries/build-stage-time.d.ts +10 -0
  14. package/dist/queries/constants.d.ts +6 -0
  15. package/dist/queries/find-item-entity-id.d.ts +2 -0
  16. package/dist/queries/types.d.ts +109 -0
  17. package/dist/sink/is-outgoing-fact.d.ts +3 -0
  18. package/dist/sink/pick-sink-answer.d.ts +7 -0
  19. package/dist/sink/to-wire-fact.d.ts +5 -0
  20. package/dist/types.d.ts +6 -0
  21. package/dist/wire-fact-schemas.d.ts +319 -0
  22. package/dist/work/boxxy-work-consumer.d.ts +125 -0
  23. package/dist/work/constants.d.ts +3 -0
  24. package/dist/work/fold-work-facts.d.ts +2 -0
  25. package/dist/work/types.d.ts +147 -0
  26. package/package.json +51 -4
  27. package/src/boxxy-source.ts +25 -0
  28. package/src/boxxy-work-plugin.ts +11 -0
  29. package/src/build-boxxy-plugin.ts +18 -0
  30. package/src/consumers/apply-scope.ts +125 -0
  31. package/src/create-pepys-fact-sink.ts +64 -0
  32. package/src/create-pepys-reader.ts +79 -0
  33. package/src/generated/boxxy-work-boundary.ts +2923 -0
  34. package/src/generated/boxxy-work.sql +71 -0
  35. package/src/index.ts +17 -0
  36. package/src/queries/build-cost-to-land-query.ts +127 -0
  37. package/src/queries/build-cost-to-land.ts +226 -0
  38. package/src/queries/build-stage-time-query.ts +59 -0
  39. package/src/queries/build-stage-time.ts +195 -0
  40. package/src/queries/constants.ts +10 -0
  41. package/src/queries/find-item-entity-id.ts +16 -0
  42. package/src/queries/types.ts +168 -0
  43. package/src/sink/is-outgoing-fact.ts +19 -0
  44. package/src/sink/pick-sink-answer.ts +29 -0
  45. package/src/sink/to-wire-fact.ts +31 -0
  46. package/src/test-utils/build-foreign-stand-in.ts +139 -0
  47. package/src/test-utils/build-mock-applied-fact.ts +39 -0
  48. package/src/test-utils/build-mock-attempt-read-row.ts +17 -0
  49. package/src/test-utils/build-mock-binding-read-row.ts +12 -0
  50. package/src/test-utils/build-mock-item-facts.ts +149 -0
  51. package/src/test-utils/build-mock-request-read-row.ts +20 -0
  52. package/src/test-utils/build-mock-scope-fact.ts +13 -0
  53. package/src/test-utils/create-ingest-stand-in.ts +56 -0
  54. package/src/test-utils/create-query-stand-in.ts +51 -0
  55. package/src/test-utils/create-test-pepys.ts +228 -0
  56. package/src/test-utils/to-entity-id.ts +14 -0
  57. package/src/test-utils/to-scope-fact.ts +92 -0
  58. package/src/types.ts +14 -0
  59. package/src/wire-fact-schemas.ts +41 -0
  60. package/src/work/boxxy-work-consumer.ts +95 -0
  61. package/src/work/constants.ts +25 -0
  62. package/src/work/fold-work-facts.ts +490 -0
  63. package/src/work/types.ts +177 -0
@@ -0,0 +1,490 @@
1
+ import type {
2
+ AttemptClaimedFact,
3
+ AttemptFold,
4
+ AttemptMarkFact,
5
+ AttemptSessionFact,
6
+ AttemptSettledFact,
7
+ ItemClosedFact,
8
+ ItemEditedFact,
9
+ ItemFiledFact,
10
+ ItemFold,
11
+ PullRequestFold,
12
+ SessionFold,
13
+ SleepStretch,
14
+ WorkFact,
15
+ WorkFold,
16
+ } from './types.ts';
17
+
18
+ // a fact's place in time: its time, then its fact ID
19
+ interface Stamp {
20
+ at: string;
21
+ id: string;
22
+ }
23
+
24
+ interface SessionDraft {
25
+ session: string;
26
+ key: 'atc' | 'agent';
27
+ product: string | null;
28
+ at: string;
29
+ id: string;
30
+ }
31
+
32
+ interface PullRequestDraft {
33
+ id: string;
34
+ at: string;
35
+ evidence: string;
36
+ }
37
+
38
+ interface AttemptDraft {
39
+ id: string;
40
+ claimed: AttemptClaimedFact | undefined;
41
+
42
+ // the earliest `started` or `reconciled`: the start time and the atc session
43
+ started: AttemptSessionFact | undefined;
44
+
45
+ // the latest `started`, `reconciled` or `resumed`: the caller
46
+ caller: AttemptSessionFact | undefined;
47
+ closedOut: AttemptMarkFact | undefined;
48
+ settled: AttemptSettledFact | undefined;
49
+ slept: Stamp[];
50
+ woken: Stamp[];
51
+ sessions: SessionDraft[];
52
+ pullRequests: PullRequestDraft[];
53
+ updatedAt: string;
54
+ }
55
+
56
+ interface TextEdit {
57
+ value: string;
58
+ at: string;
59
+ id: string;
60
+ }
61
+
62
+ interface ItemDraft {
63
+ filed: ItemFiledFact | undefined;
64
+ title: TextEdit | undefined;
65
+ kind: TextEdit | undefined;
66
+ designMode: TextEdit | undefined;
67
+ closed: ItemClosedFact | undefined;
68
+ }
69
+
70
+ // Two facts with one time are ordered by fact ID, so every arrival order gives the same fold.
71
+ // scriptc compiles this file into the consumer, so it uses loops, plain objects and text
72
+ // compares, and no Map, Set or Date.
73
+ export function foldWorkFacts(facts: readonly WorkFact[]): WorkFold {
74
+ const item: ItemDraft = {
75
+ filed: undefined,
76
+ title: undefined,
77
+ kind: undefined,
78
+ designMode: undefined,
79
+ closed: undefined,
80
+ };
81
+
82
+ const attempts: AttemptDraft[] = [];
83
+
84
+ for (const fact of facts) {
85
+ if (fact.type === 'boxxy.item.filed') {
86
+ if (item.filed === undefined || isEarlier(fact, item.filed)) {
87
+ item.filed = fact;
88
+ }
89
+ } else if (fact.type === 'boxxy.item.edited') {
90
+ layEdit(item, fact);
91
+ } else if (fact.type === 'boxxy.item.closed') {
92
+ if (item.closed === undefined || isEarlier(item.closed, fact)) {
93
+ item.closed = fact;
94
+ }
95
+ } else if (fact.type === 'boxxy.attempt.claimed') {
96
+ layClaimed(getDraft(attempts, fact.payload.attempt, fact.at), fact);
97
+ } else if (
98
+ fact.type === 'boxxy.attempt.started' ||
99
+ fact.type === 'boxxy.attempt.reconciled' ||
100
+ fact.type === 'boxxy.attempt.resumed'
101
+ ) {
102
+ laySession(getDraft(attempts, fact.payload.attempt, fact.at), fact);
103
+ } else if (fact.type === 'boxxy.attempt.session_bound') {
104
+ const draft = getDraft(attempts, fact.payload.attempt, fact.at);
105
+
106
+ // the atc row too: `started` may be a parked outbox row that arrives later, or never
107
+ bindSession(draft, {
108
+ session: fact.payload.atcSession,
109
+ key: 'atc',
110
+ product: null,
111
+ at: fact.at,
112
+ id: fact.id,
113
+ });
114
+
115
+ bindSession(draft, {
116
+ session: fact.payload.agentSession,
117
+ key: 'agent',
118
+ product: fact.payload.product,
119
+ at: fact.at,
120
+ id: fact.id,
121
+ });
122
+ } else if (fact.type === 'boxxy.attempt.slept') {
123
+ getDraft(attempts, fact.payload.attempt, fact.at).slept.push({ at: fact.at, id: fact.id });
124
+ } else if (fact.type === 'boxxy.attempt.woken') {
125
+ getDraft(attempts, fact.payload.attempt, fact.at).woken.push({ at: fact.at, id: fact.id });
126
+ } else if (fact.type === 'boxxy.attempt.closed_out') {
127
+ layClosedOut(getDraft(attempts, fact.payload.attempt, fact.at), fact);
128
+ } else if (fact.type === 'boxxy.attempt.settled') {
129
+ laySettled(getDraft(attempts, fact.payload.attempt, fact.at), fact);
130
+ } else if (fact.type === 'boxxy.pull_request.linked') {
131
+ const draft = getDraft(attempts, fact.payload.attempt, fact.at);
132
+
133
+ linkPullRequest(draft, { id: fact.payload.pullRequestId, at: fact.at, evidence: fact.id });
134
+ }
135
+ }
136
+
137
+ return { item: buildItemFold(item), attempts: collectAttemptFolds(attempts) };
138
+ }
139
+
140
+ function isEarlier(left: Stamp, right: Stamp): boolean {
141
+ return left.at < right.at || (left.at === right.at && left.id < right.id);
142
+ }
143
+
144
+ // Each field of an edit is laid over the row by itself: the latest edit that names the field wins.
145
+ function layEdit(item: ItemDraft, fact: ItemEditedFact): void {
146
+ const at = fact.at;
147
+ const id = fact.id;
148
+ const payload = fact.payload;
149
+
150
+ if (typeof payload.title === 'string') {
151
+ item.title = pickLaterEdit(item.title, { value: payload.title, at, id });
152
+ }
153
+
154
+ if (typeof payload.kind === 'string') {
155
+ item.kind = pickLaterEdit(item.kind, { value: payload.kind, at, id });
156
+ }
157
+
158
+ const designMode = findDesignMode(payload.fields);
159
+
160
+ if (designMode !== null) {
161
+ item.designMode = pickLaterEdit(item.designMode, { value: designMode, at, id });
162
+ }
163
+ }
164
+
165
+ function pickLaterEdit(current: TextEdit | undefined, next: TextEdit): TextEdit {
166
+ return current === undefined || isEarlier(current, next) ? next : current;
167
+ }
168
+
169
+ function findDesignMode(fields: unknown): string | null {
170
+ if (typeof fields !== 'object' || fields === null || !('designMode' in fields)) {
171
+ return null;
172
+ }
173
+
174
+ return typeof fields.designMode === 'string' ? fields.designMode : null;
175
+ }
176
+
177
+ function layClaimed(draft: AttemptDraft, fact: AttemptClaimedFact): void {
178
+ if (draft.claimed === undefined || isEarlier(fact, draft.claimed)) {
179
+ draft.claimed = fact;
180
+ }
181
+ }
182
+
183
+ // The draft of one attempt, made on first sight and kept in attempt ID order.
184
+ function getDraft(drafts: AttemptDraft[], id: string, at: string): AttemptDraft {
185
+ for (const draft of drafts) {
186
+ if (draft.id === id) {
187
+ if (draft.updatedAt < at) {
188
+ draft.updatedAt = at;
189
+ }
190
+
191
+ return draft;
192
+ }
193
+ }
194
+
195
+ const draft: AttemptDraft = {
196
+ id,
197
+ claimed: undefined,
198
+ started: undefined,
199
+ caller: undefined,
200
+ closedOut: undefined,
201
+ settled: undefined,
202
+ slept: [],
203
+ woken: [],
204
+ sessions: [],
205
+ pullRequests: [],
206
+ updatedAt: at,
207
+ };
208
+
209
+ drafts.push(draft);
210
+
211
+ for (let index = drafts.length - 1; index > 0; index -= 1) {
212
+ const before = drafts[index - 1];
213
+
214
+ if (before === undefined || before.id < id) {
215
+ break;
216
+ }
217
+
218
+ drafts[index] = before;
219
+ drafts[index - 1] = draft;
220
+ }
221
+
222
+ return draft;
223
+ }
224
+
225
+ // A wake keeps the session, so `resumed` names the atc session the attempt already has, unless
226
+ // the worker answered with a new one. It sets the caller but not the start.
227
+ function laySession(draft: AttemptDraft, fact: AttemptSessionFact): void {
228
+ const isStart = fact.type !== 'boxxy.attempt.resumed';
229
+
230
+ if (isStart && (draft.started === undefined || isEarlier(fact, draft.started))) {
231
+ draft.started = fact;
232
+ }
233
+
234
+ if (draft.caller === undefined || isEarlier(draft.caller, fact)) {
235
+ draft.caller = fact;
236
+ }
237
+
238
+ bindSession(draft, {
239
+ session: fact.payload.atcSession,
240
+ key: 'atc',
241
+ product: null,
242
+ at: fact.at,
243
+ id: fact.id,
244
+ });
245
+ }
246
+
247
+ // One row per session of the attempt: the earliest fact that names the session gives its key,
248
+ // product, time and evidence.
249
+ function bindSession(draft: AttemptDraft, binding: SessionDraft): void {
250
+ for (let index = 0; index < draft.sessions.length; index += 1) {
251
+ const bound = draft.sessions[index];
252
+
253
+ if (bound !== undefined && bound.session === binding.session) {
254
+ if (isEarlier(binding, bound)) {
255
+ draft.sessions[index] = binding;
256
+ }
257
+
258
+ return;
259
+ }
260
+ }
261
+
262
+ draft.sessions.push(binding);
263
+
264
+ for (let index = draft.sessions.length - 1; index > 0; index -= 1) {
265
+ const before = draft.sessions[index - 1];
266
+
267
+ if (before === undefined || before.session < binding.session) {
268
+ break;
269
+ }
270
+
271
+ draft.sessions[index] = before;
272
+ draft.sessions[index - 1] = binding;
273
+ }
274
+ }
275
+
276
+ function layClosedOut(draft: AttemptDraft, fact: AttemptMarkFact): void {
277
+ if (draft.closedOut === undefined || isEarlier(fact, draft.closedOut)) {
278
+ draft.closedOut = fact;
279
+ }
280
+ }
281
+
282
+ function laySettled(draft: AttemptDraft, fact: AttemptSettledFact): void {
283
+ if (draft.settled === undefined || isEarlier(fact, draft.settled)) {
284
+ draft.settled = fact;
285
+ }
286
+ }
287
+
288
+ // The hook and the watch may both link one pull request: one link, with the earliest fact as its
289
+ // evidence.
290
+ function linkPullRequest(draft: AttemptDraft, link: PullRequestDraft): void {
291
+ for (let index = 0; index < draft.pullRequests.length; index += 1) {
292
+ const linked = draft.pullRequests[index];
293
+
294
+ if (linked !== undefined && linked.id === link.id) {
295
+ if (isEarlier({ at: link.at, id: link.evidence }, { at: linked.at, id: linked.evidence })) {
296
+ draft.pullRequests[index] = link;
297
+ }
298
+
299
+ return;
300
+ }
301
+ }
302
+
303
+ draft.pullRequests.push(link);
304
+
305
+ for (let index = draft.pullRequests.length - 1; index > 0; index -= 1) {
306
+ const before = draft.pullRequests[index - 1];
307
+
308
+ if (before === undefined || before.id < link.id) {
309
+ break;
310
+ }
311
+
312
+ draft.pullRequests[index] = before;
313
+ draft.pullRequests[index - 1] = link;
314
+ }
315
+ }
316
+
317
+ // An item with no `filed` fact in the scope gives no item row.
318
+ function buildItemFold(item: ItemDraft): ItemFold | null {
319
+ const filed = item.filed;
320
+
321
+ if (filed === undefined) {
322
+ return null;
323
+ }
324
+
325
+ let updatedAt = filed.at;
326
+
327
+ for (const edit of [item.title, item.kind, item.designMode]) {
328
+ if (edit !== undefined && updatedAt < edit.at) {
329
+ updatedAt = edit.at;
330
+ }
331
+ }
332
+
333
+ if (item.closed !== undefined && updatedAt < item.closed.at) {
334
+ updatedAt = item.closed.at;
335
+ }
336
+
337
+ return {
338
+ kind: item.kind === undefined ? filed.payload.kind : item.kind.value,
339
+ repo: filed.payload.repo,
340
+ title: item.title === undefined ? filed.payload.title : item.title.value,
341
+ design_mode:
342
+ item.designMode === undefined ? findDesignMode(filed.payload.fields) : item.designMode.value,
343
+ filed_at: filed.at,
344
+ closed_at: item.closed === undefined ? null : item.closed.at,
345
+ close_reason: item.closed === undefined ? null : item.closed.payload.reason,
346
+ updated_at: updatedAt,
347
+ };
348
+ }
349
+
350
+ // An attempt with no `claimed` fact in the scope gives nothing: its row is parked in boxxy's
351
+ // outbox, or not sent yet, and the claimed fact marks the scope dirty when it arrives.
352
+ function collectAttemptFolds(drafts: readonly AttemptDraft[]): AttemptFold[] {
353
+ const folds: AttemptFold[] = [];
354
+
355
+ for (const draft of drafts) {
356
+ const claimed = draft.claimed;
357
+
358
+ if (claimed !== undefined) {
359
+ folds.push(buildAttemptFold(draft, claimed));
360
+ }
361
+ }
362
+
363
+ return folds;
364
+ }
365
+
366
+ function buildAttemptFold(draft: AttemptDraft, claimed: AttemptClaimedFact): AttemptFold {
367
+ const settled = draft.settled;
368
+ const caller = draft.caller === undefined ? null : draft.caller.payload.caller;
369
+
370
+ return {
371
+ id: draft.id,
372
+ phase: claimed.payload.phase,
373
+ number: claimed.payload.number,
374
+ round: claimed.payload.round,
375
+ mode: claimed.payload.mode,
376
+ pool: claimed.payload.pool,
377
+ recipe_hash: claimed.payload.recipeHash,
378
+ rules_version: claimed.payload.rulesVersion,
379
+ atc_session: draft.started === undefined ? null : draft.started.payload.atcSession,
380
+ state: settled === undefined ? 'live' : settled.payload.state,
381
+ outcome: settled === undefined ? null : settled.payload.outcome,
382
+ failure: settled === undefined ? null : settled.payload.failure,
383
+ claimed_at: claimed.at,
384
+ started_at: draft.started === undefined ? null : draft.started.at,
385
+ closed_out_at: draft.closedOut === undefined ? null : draft.closedOut.at,
386
+ ended_at: settled === undefined ? null : settled.at,
387
+ sleeps: pairSleeps(draft.slept, draft.woken, settled),
388
+ updated_at: draft.updatedAt,
389
+ claimedFact: claimed.id,
390
+ sessions: collectSessionFolds(draft.sessions, caller),
391
+ pullRequests: collectPullRequestFolds(draft.pullRequests),
392
+ };
393
+ }
394
+
395
+ // Each sleep pairs with the earliest wake at or after it. The last sleep with no wake ends at the
396
+ // settle when the attempt settled asleep; else it is open, with `to` null. Times compare as text:
397
+ // boxxy writes every time in the one fixed form of an ISO 8601 UTC instant.
398
+ function pairSleeps(
399
+ slept: readonly Stamp[],
400
+ woken: readonly Stamp[],
401
+ settled: AttemptSettledFact | undefined,
402
+ ): SleepStretch[] {
403
+ const sleeps = sortStamps(slept);
404
+ const wakes = sortStamps(woken);
405
+ const stretches: SleepStretch[] = [];
406
+ let next = 0;
407
+
408
+ for (let index = 0; index < sleeps.length; index += 1) {
409
+ const sleep = sleeps[index];
410
+
411
+ if (sleep !== undefined) {
412
+ while (next < wakes.length && isBefore(wakes[next], sleep.at)) {
413
+ next += 1;
414
+ }
415
+
416
+ const wake = wakes[next];
417
+
418
+ if (wake === undefined) {
419
+ const isLast = index === sleeps.length - 1;
420
+ const endsAtSettle = isLast && settled !== undefined && settled.payload.asleep;
421
+
422
+ stretches.push({ from: sleep.at, to: endsAtSettle ? settled.at : null });
423
+ } else {
424
+ stretches.push({ from: sleep.at, to: wake.at });
425
+
426
+ next += 1;
427
+ }
428
+ }
429
+ }
430
+
431
+ return stretches;
432
+ }
433
+
434
+ function sortStamps(stamps: readonly Stamp[]): Stamp[] {
435
+ const sorted: Stamp[] = [];
436
+
437
+ for (const stamp of stamps) {
438
+ sorted.push(stamp);
439
+
440
+ for (let index = sorted.length - 1; index > 0; index -= 1) {
441
+ const before = sorted[index - 1];
442
+
443
+ if (before === undefined || isEarlier(before, stamp)) {
444
+ break;
445
+ }
446
+
447
+ sorted[index] = before;
448
+ sorted[index - 1] = stamp;
449
+ }
450
+ }
451
+
452
+ return sorted;
453
+ }
454
+
455
+ function isBefore(stamp: Stamp | undefined, at: string): boolean {
456
+ return stamp !== undefined && stamp.at < at;
457
+ }
458
+
459
+ // Every session row of the attempt holds the caller of its latest `started`, `reconciled` or
460
+ // `resumed`. With none, both columns are null, and no telemetry counts for the attempt.
461
+ function collectSessionFolds(
462
+ sessions: readonly SessionDraft[],
463
+ caller: { issuer: string; subject: string } | null,
464
+ ): SessionFold[] {
465
+ const folds: SessionFold[] = [];
466
+
467
+ for (const bound of sessions) {
468
+ folds.push({
469
+ session: bound.session,
470
+ key: bound.key,
471
+ product: bound.product,
472
+ caller_issuer: caller === null ? null : caller.issuer,
473
+ caller_subject: caller === null ? null : caller.subject,
474
+ bound_at: bound.at,
475
+ evidence: bound.id,
476
+ });
477
+ }
478
+
479
+ return folds;
480
+ }
481
+
482
+ function collectPullRequestFolds(links: readonly PullRequestDraft[]): PullRequestFold[] {
483
+ const folds: PullRequestFold[] = [];
484
+
485
+ for (const link of links) {
486
+ folds.push({ id: link.id, evidence: link.evidence });
487
+ }
488
+
489
+ return folds;
490
+ }
@@ -0,0 +1,177 @@
1
+ // The facts the fold reads, in the shape that the kit's glue gives the consumer, so the glue's fact
2
+ // union fits `WorkFact`. The glue types an `exactOptional` field of the model as `unknown`, so
3
+ // those fields are `unknown` here and the fold narrows them.
4
+
5
+ interface CallerFields {
6
+ issuer: string;
7
+ subject: string;
8
+ }
9
+
10
+ interface FactBase {
11
+ id: string;
12
+ entity: string;
13
+ at: string;
14
+ }
15
+
16
+ export interface ItemFiledFact extends FactBase {
17
+ type: 'boxxy.item.filed';
18
+ payload: { kind: string; repo: string; title: string; fields: { designMode?: unknown } };
19
+ }
20
+
21
+ export interface ItemEditedFact extends FactBase {
22
+ type: 'boxxy.item.edited';
23
+ payload: { title?: unknown; kind?: unknown; fields?: unknown };
24
+ }
25
+
26
+ export interface ItemClosedFact extends FactBase {
27
+ type: 'boxxy.item.closed';
28
+ payload: { reason: string };
29
+ }
30
+
31
+ export interface AttemptClaimedFact extends FactBase {
32
+ type: 'boxxy.attempt.claimed';
33
+ payload: {
34
+ attempt: string;
35
+ phase: string;
36
+ number: number;
37
+ round: number;
38
+ mode: string;
39
+ pool: string;
40
+ recipeHash: string;
41
+ rulesVersion: string;
42
+ };
43
+ }
44
+
45
+ // `started`, `reconciled` and `resumed`: the facts that carry the attempt's atc session and caller
46
+ export interface AttemptSessionFact extends FactBase {
47
+ type: 'boxxy.attempt.started' | 'boxxy.attempt.reconciled' | 'boxxy.attempt.resumed';
48
+ payload: { attempt: string; atcSession: string; caller: CallerFields | null };
49
+ }
50
+
51
+ interface SessionBoundFact extends FactBase {
52
+ type: 'boxxy.attempt.session_bound';
53
+ payload: { attempt: string; atcSession: string; agentSession: string; product: string };
54
+ }
55
+
56
+ // `slept`, `woken` and `closed_out`: the fold reads the attempt and the time
57
+ export interface AttemptMarkFact extends FactBase {
58
+ type: 'boxxy.attempt.slept' | 'boxxy.attempt.woken' | 'boxxy.attempt.closed_out';
59
+ payload: { attempt: string };
60
+ }
61
+
62
+ export interface AttemptSettledFact extends FactBase {
63
+ type: 'boxxy.attempt.settled';
64
+ payload: {
65
+ attempt: string;
66
+ state: string;
67
+ outcome: string;
68
+ failure: string | null;
69
+ asleep: boolean;
70
+ };
71
+ }
72
+
73
+ interface PullRequestLinkedFact extends FactBase {
74
+ type: 'boxxy.pull_request.linked';
75
+ payload: { attempt: string; pullRequestId: string };
76
+ }
77
+
78
+ // the 14 fact types the fold passes over
79
+ interface PassedOverFact extends FactBase {
80
+ type:
81
+ | 'boxxy.item.noted'
82
+ | 'boxxy.relation.added'
83
+ | 'boxxy.relation.removed'
84
+ | 'boxxy.lever.recorded'
85
+ | 'boxxy.decision.recorded'
86
+ | 'boxxy.attempt.spawn_unanswered'
87
+ | 'boxxy.attempt.wake_requested'
88
+ | 'boxxy.attempt.timed_out'
89
+ | 'boxxy.attempt.turn_ended'
90
+ | 'boxxy.proposal.recorded'
91
+ | 'boxxy.evidence.recorded'
92
+ | 'boxxy.commit.linked'
93
+ | 'boxxy.pull_request.merged'
94
+ | 'boxxy.pull_request.closed';
95
+ payload?: unknown;
96
+ }
97
+
98
+ export type WorkFact =
99
+ | ItemFiledFact
100
+ | ItemEditedFact
101
+ | ItemClosedFact
102
+ | AttemptClaimedFact
103
+ | AttemptSessionFact
104
+ | SessionBoundFact
105
+ | AttemptMarkFact
106
+ | AttemptSettledFact
107
+ | PullRequestLinkedFact
108
+ | PassedOverFact;
109
+
110
+ // What the fold gives: the rows of the three tables and the evidence of each link, with bare
111
+ // IDs. The entry file turns each bare ID into its entity ID with the glue's builders.
112
+
113
+ // `boxxy_item` without `id`: the scope is the item
114
+ export interface ItemFold {
115
+ kind: string;
116
+ repo: string;
117
+ title: string;
118
+ design_mode: string | null;
119
+ filed_at: string;
120
+ closed_at: string | null;
121
+ close_reason: string | null;
122
+ updated_at: string;
123
+ }
124
+
125
+ export interface SleepStretch {
126
+ from: string;
127
+ to: string | null;
128
+ }
129
+
130
+ // `boxxy_attempt` with the bare attempt ID, and without `item_id`
131
+ export interface AttemptFold {
132
+ id: string;
133
+ phase: string;
134
+ number: number;
135
+ round: number;
136
+ mode: string;
137
+ pool: string;
138
+ recipe_hash: string;
139
+ rules_version: string;
140
+ atc_session: string | null;
141
+ state: string;
142
+ outcome: string | null;
143
+ failure: string | null;
144
+ claimed_at: string;
145
+ started_at: string | null;
146
+ closed_out_at: string | null;
147
+ ended_at: string | null;
148
+ sleeps: SleepStretch[];
149
+ updated_at: string;
150
+
151
+ // the `claimed` fact: the evidence of `attempt_of`
152
+ claimedFact: string;
153
+ sessions: SessionFold[];
154
+ pullRequests: PullRequestFold[];
155
+ }
156
+
157
+ // one `boxxy_attempt_session` row, with the bare session ID, and the evidence of its `ran_in`
158
+ export interface SessionFold {
159
+ session: string;
160
+ key: 'atc' | 'agent';
161
+ product: string | null;
162
+ caller_issuer: string | null;
163
+ caller_subject: string | null;
164
+ bound_at: string;
165
+ evidence: string;
166
+ }
167
+
168
+ // one `produced` link: the decimal text of GitHub's ID, and its evidence
169
+ export interface PullRequestFold {
170
+ id: string;
171
+ evidence: string;
172
+ }
173
+
174
+ export interface WorkFold {
175
+ item: ItemFold | null;
176
+ attempts: AttemptFold[];
177
+ }