@boxxy/pepys 0.0.1 → 0.1.3
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/README.md +42 -5
- package/dist/boxxy-source.d.ts +636 -0
- package/dist/boxxy-work-plugin.d.ts +1 -0
- package/dist/build-boxxy-plugin.d.ts +181 -0
- package/dist/consumers/apply-scope.d.ts +5 -0
- package/dist/create-pepys-fact-sink.d.ts +9 -0
- package/dist/create-pepys-reader.d.ts +9 -0
- package/dist/generated/boxxy-work-boundary.d.ts +566 -0
- package/dist/index.d.ts +5 -0
- package/dist/queries/build-cost-to-land-query.d.ts +156 -0
- package/dist/queries/build-cost-to-land.d.ts +12 -0
- package/dist/queries/build-stage-time-query.d.ts +29 -0
- package/dist/queries/build-stage-time.d.ts +10 -0
- package/dist/queries/constants.d.ts +6 -0
- package/dist/queries/find-item-entity-id.d.ts +2 -0
- package/dist/queries/types.d.ts +109 -0
- package/dist/sink/is-outgoing-fact.d.ts +3 -0
- package/dist/sink/pick-sink-answer.d.ts +7 -0
- package/dist/sink/to-wire-fact.d.ts +5 -0
- package/dist/types.d.ts +6 -0
- package/dist/wire-fact-schemas.d.ts +319 -0
- package/dist/work/boxxy-work-consumer.d.ts +125 -0
- package/dist/work/constants.d.ts +3 -0
- package/dist/work/fold-work-facts.d.ts +2 -0
- package/dist/work/types.d.ts +147 -0
- package/package.json +45 -4
- package/src/boxxy-source.ts +25 -0
- package/src/boxxy-work-plugin.ts +11 -0
- package/src/build-boxxy-plugin.ts +18 -0
- package/src/consumers/apply-scope.ts +125 -0
- package/src/create-pepys-fact-sink.ts +64 -0
- package/src/create-pepys-reader.ts +79 -0
- package/src/generated/boxxy-work-boundary.ts +2923 -0
- package/src/generated/boxxy-work.sql +71 -0
- package/src/index.ts +17 -0
- package/src/queries/build-cost-to-land-query.ts +127 -0
- package/src/queries/build-cost-to-land.ts +226 -0
- package/src/queries/build-stage-time-query.ts +59 -0
- package/src/queries/build-stage-time.ts +188 -0
- package/src/queries/constants.ts +10 -0
- package/src/queries/find-item-entity-id.ts +16 -0
- package/src/queries/types.ts +168 -0
- package/src/sink/is-outgoing-fact.ts +19 -0
- package/src/sink/pick-sink-answer.ts +29 -0
- package/src/sink/to-wire-fact.ts +31 -0
- package/src/types.ts +14 -0
- package/src/wire-fact-schemas.ts +41 -0
- package/src/work/boxxy-work-consumer.ts +95 -0
- package/src/work/constants.ts +25 -0
- package/src/work/fold-work-facts.ts +490 -0
- 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
|
+
}
|