@defrex/autobuild 0.9.0 → 0.10.0
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 +1 -1
- package/SPEC.md +10 -0
- package/docs/architecture.md +109 -0
- package/docs/configuration.md +24 -3
- package/docs/remote-store-protocol.md +35 -0
- package/docs/website.md +4 -0
- package/package.json +6 -4
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +85 -147
- package/packages/core/src/cli/dashboard/poll.ts +7 -2
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +80 -37
- package/packages/core/src/cli/help.ts +15 -1
- package/packages/core/src/cli/init-validation.ts +18 -0
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +1 -1
- package/packages/core/src/cli/ticket.ts +146 -1
- package/packages/core/src/cli/wait.ts +10 -2
- package/packages/core/src/cli/watch.ts +10 -2
- package/packages/core/src/config/live.ts +1 -0
- package/packages/core/src/config/pipeline-source.ts +17 -5
- package/packages/core/src/config/schema.ts +21 -0
- package/packages/core/src/integration/harness.ts +113 -1
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +43 -9
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +216 -74
- package/packages/core/src/operator/query.ts +68 -28
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +32 -1
- package/packages/core/src/processes/build-execution-state.ts +31 -15
- package/packages/core/src/processes/build-runner.ts +184 -90
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +270 -176
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +44 -3
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +26 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +10 -1
- package/packages/core/src/store/build-scope.ts +26 -0
- package/packages/core/src/store/contract.ts +337 -0
- package/packages/core/src/store/digest.ts +71 -18
- package/packages/core/src/store/local/schema.ts +17 -0
- package/packages/core/src/store/local/store.ts +111 -4
- package/packages/core/src/store/memory.ts +68 -1
- package/packages/core/src/store/phase-session.ts +23 -0
- package/packages/core/src/store/remote/client.ts +50 -1
- package/packages/core/src/store/remote/index.ts +2 -0
- package/packages/core/src/store/remote/protocol.ts +15 -0
- package/packages/core/src/store/repo-state-events.ts +72 -7
- package/packages/core/src/store/session-handle.ts +30 -0
- package/packages/core/src/store/session-reducer.ts +52 -11
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/ticket-assets.ts +41 -8
- package/packages/core/src/store/types.ts +38 -0
- package/skills/guide/SKILL.md +26 -7
- package/skills/guide/references/remote-store-protocol.md +35 -0
- package/skills/tickets/SKILL.md +57 -5
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { graphemes } from './cells'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Deterministic parsing of a rendered dashboard frame.
|
|
5
|
+
*
|
|
6
|
+
* The dashboard deliberately emits a tiny terminal vocabulary: SGR reset,
|
|
7
|
+
* bold/dim and six named foreground colours, plus OSC 8 hyperlinks. This
|
|
8
|
+
* parser accepts exactly that vocabulary and nothing else. Unknown control
|
|
9
|
+
* traffic is an error rather than evidence that merely looks plausible after
|
|
10
|
+
* bytes were dropped. Every rendition of a frame — the PNG evidence, the
|
|
11
|
+
* website's HTML — derives from these same exact cells.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const ESC = '\x1b'
|
|
15
|
+
const BEL = '\x07'
|
|
16
|
+
|
|
17
|
+
export type Foreground = 'foreground' | 'red' | 'green' | 'yellow' | 'blue' | 'cyan'
|
|
18
|
+
|
|
19
|
+
export interface FrameStyle {
|
|
20
|
+
foreground: Foreground
|
|
21
|
+
bold: boolean
|
|
22
|
+
dim: boolean
|
|
23
|
+
href?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface FrameRun {
|
|
27
|
+
/** Zero-based terminal cell column the run starts at. */
|
|
28
|
+
column: number
|
|
29
|
+
/** Terminal cells the run occupies. */
|
|
30
|
+
cells: number
|
|
31
|
+
text: string
|
|
32
|
+
style: FrameStyle
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface ParsedFrameLine {
|
|
36
|
+
/** The line with every escape removed. */
|
|
37
|
+
text: string
|
|
38
|
+
/** Terminal cells the whole line occupies. */
|
|
39
|
+
cells: number
|
|
40
|
+
runs: FrameRun[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function cloneStyle(style: FrameStyle): FrameStyle {
|
|
44
|
+
return {
|
|
45
|
+
foreground: style.foreground,
|
|
46
|
+
bold: style.bold,
|
|
47
|
+
dim: style.dim,
|
|
48
|
+
...(style.href !== undefined ? { href: style.href } : {}),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function sameStyle(left: FrameStyle, right: FrameStyle): boolean {
|
|
53
|
+
return (
|
|
54
|
+
left.foreground === right.foreground &&
|
|
55
|
+
left.bold === right.bold &&
|
|
56
|
+
left.dim === right.dim &&
|
|
57
|
+
left.href === right.href
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function applySgr(style: FrameStyle, raw: string, line: number): void {
|
|
62
|
+
const codes = raw === '' ? [0] : raw.split(';').map((part) => Number(part))
|
|
63
|
+
if (codes.some((code) => !Number.isInteger(code))) {
|
|
64
|
+
throw new Error(`dashboard frame line ${line}: malformed SGR sequence ESC[${raw}m`)
|
|
65
|
+
}
|
|
66
|
+
for (const code of codes) {
|
|
67
|
+
switch (code) {
|
|
68
|
+
case 0:
|
|
69
|
+
style.foreground = 'foreground'
|
|
70
|
+
style.bold = false
|
|
71
|
+
style.dim = false
|
|
72
|
+
break
|
|
73
|
+
case 1:
|
|
74
|
+
style.bold = true
|
|
75
|
+
break
|
|
76
|
+
case 2:
|
|
77
|
+
style.dim = true
|
|
78
|
+
break
|
|
79
|
+
case 22:
|
|
80
|
+
style.bold = false
|
|
81
|
+
style.dim = false
|
|
82
|
+
break
|
|
83
|
+
case 31:
|
|
84
|
+
style.foreground = 'red'
|
|
85
|
+
break
|
|
86
|
+
case 32:
|
|
87
|
+
style.foreground = 'green'
|
|
88
|
+
break
|
|
89
|
+
case 33:
|
|
90
|
+
style.foreground = 'yellow'
|
|
91
|
+
break
|
|
92
|
+
case 34:
|
|
93
|
+
style.foreground = 'blue'
|
|
94
|
+
break
|
|
95
|
+
case 36:
|
|
96
|
+
style.foreground = 'cyan'
|
|
97
|
+
break
|
|
98
|
+
case 39:
|
|
99
|
+
style.foreground = 'foreground'
|
|
100
|
+
break
|
|
101
|
+
default:
|
|
102
|
+
throw new Error(`dashboard frame line ${line}: unsupported SGR code ${code} in ESC[${raw}m`)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Parse one rendered line into styled runs pinned to their terminal cells.
|
|
108
|
+
* `lineNumber` is one-based and only names the line in errors. */
|
|
109
|
+
export function parseFrameLine(value: string, lineNumber: number): ParsedFrameLine {
|
|
110
|
+
const style: FrameStyle = {
|
|
111
|
+
foreground: 'foreground',
|
|
112
|
+
bold: false,
|
|
113
|
+
dim: false,
|
|
114
|
+
}
|
|
115
|
+
const runs: FrameRun[] = []
|
|
116
|
+
let text = ''
|
|
117
|
+
let cells = 0
|
|
118
|
+
|
|
119
|
+
const append = (cluster: string, width: number): void => {
|
|
120
|
+
const previous = runs.at(-1)
|
|
121
|
+
// ASCII runs may coalesce. Unicode clusters stay independently pinned to
|
|
122
|
+
// their terminal cell columns so font fallback/advance cannot shift the
|
|
123
|
+
// run that follows a wide glyph.
|
|
124
|
+
if (
|
|
125
|
+
previous !== undefined &&
|
|
126
|
+
previous.column + previous.cells === cells &&
|
|
127
|
+
/^[\x20-\x7e]*$/.test(previous.text) &&
|
|
128
|
+
/^[\x20-\x7e]$/.test(cluster) &&
|
|
129
|
+
sameStyle(previous.style, style)
|
|
130
|
+
) {
|
|
131
|
+
previous.text += cluster
|
|
132
|
+
previous.cells += width
|
|
133
|
+
} else {
|
|
134
|
+
runs.push({ column: cells, cells: width, text: cluster, style: cloneStyle(style) })
|
|
135
|
+
}
|
|
136
|
+
text += cluster
|
|
137
|
+
cells += width
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for (let index = 0; index < value.length; ) {
|
|
141
|
+
if (value[index] === ESC) {
|
|
142
|
+
const family = value[index + 1]
|
|
143
|
+
if (family === '[') {
|
|
144
|
+
const rest = value.slice(index + 2)
|
|
145
|
+
const match = /^([0-9;]*)m/.exec(rest)
|
|
146
|
+
if (match === null) {
|
|
147
|
+
throw new Error(
|
|
148
|
+
`dashboard frame line ${lineNumber}: unsupported or unterminated CSI sequence`,
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
applySgr(style, match[1]!, lineNumber)
|
|
152
|
+
index += 2 + match[0].length
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
if (family === ']') {
|
|
156
|
+
const end = value.indexOf(BEL, index + 2)
|
|
157
|
+
if (end === -1) {
|
|
158
|
+
throw new Error(`dashboard frame line ${lineNumber}: unterminated OSC sequence`)
|
|
159
|
+
}
|
|
160
|
+
const payload = value.slice(index + 2, end)
|
|
161
|
+
const match = /^8;;(.*)$/.exec(payload)
|
|
162
|
+
if (match === null) {
|
|
163
|
+
throw new Error(
|
|
164
|
+
`dashboard frame line ${lineNumber}: unsupported OSC sequence ${JSON.stringify(payload)}`,
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
const href = match[1]!
|
|
168
|
+
style.href = href === '' ? undefined : href
|
|
169
|
+
index = end + 1
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
throw new Error(
|
|
173
|
+
`dashboard frame line ${lineNumber}: unsupported escape family ${JSON.stringify(family ?? '')}`,
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const nextEscape = value.indexOf(ESC, index)
|
|
178
|
+
const stop = nextEscape === -1 ? value.length : nextEscape
|
|
179
|
+
const plain = value.slice(index, stop)
|
|
180
|
+
for (const character of plain) {
|
|
181
|
+
const code = character.codePointAt(0)!
|
|
182
|
+
if (code < 0x20 || code === 0x7f || (code >= 0x80 && code <= 0x9f)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`dashboard frame line ${lineNumber}: unsupported control U+${code
|
|
185
|
+
.toString(16)
|
|
186
|
+
.toUpperCase()
|
|
187
|
+
.padStart(4, '0')}`,
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
for (const cluster of graphemes(plain)) append(cluster.text, cluster.width)
|
|
192
|
+
index = stop
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (style.href !== undefined) {
|
|
196
|
+
throw new Error(`dashboard frame line ${lineNumber}: OSC 8 hyperlink was not closed`)
|
|
197
|
+
}
|
|
198
|
+
if (style.foreground !== 'foreground' || style.bold || style.dim) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`dashboard frame line ${lineNumber}: SGR style was not reset before end of line`,
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
return { text, cells, runs }
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Parse a whole frame, holding every line to the declared terminal width. */
|
|
207
|
+
export function parseFrame(lines: readonly string[], columns: number): ParsedFrameLine[] {
|
|
208
|
+
if (!Number.isInteger(columns) || columns <= 0) {
|
|
209
|
+
throw new Error(`dashboard frame columns must be a positive integer, got ${columns}`)
|
|
210
|
+
}
|
|
211
|
+
if (lines.length === 0) {
|
|
212
|
+
throw new Error('dashboard frame is empty')
|
|
213
|
+
}
|
|
214
|
+
const parsed = lines.map((line, index) => parseFrameLine(line, index + 1))
|
|
215
|
+
for (const [index, line] of parsed.entries()) {
|
|
216
|
+
if (line.cells > columns) {
|
|
217
|
+
throw new Error(
|
|
218
|
+
`dashboard frame line ${index + 1} is ${line.cells} cells wide, exceeding declared terminal width ${columns}`,
|
|
219
|
+
)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return parsed
|
|
223
|
+
}
|
|
@@ -40,9 +40,20 @@ import type { RepositoryEvent } from '../../events/repository'
|
|
|
40
40
|
import type { Config } from '../../config/schema'
|
|
41
41
|
import type { PipelineSourceMeta } from '../../config/pipeline-source'
|
|
42
42
|
import type { BuildState, PhaseContext, PrLifecycle } from '../../kernel/reducer'
|
|
43
|
-
import {
|
|
44
|
-
|
|
43
|
+
import {
|
|
44
|
+
currentAutoMergeDeferralFrom,
|
|
45
|
+
currentDeferralObservationReducer,
|
|
46
|
+
type DeferralLedger,
|
|
47
|
+
} from '../../kernel/auto-merge'
|
|
48
|
+
import { decideNextFromFacts } from '../../kernel/engine'
|
|
45
49
|
import { verifyPhase } from '../../ontology'
|
|
50
|
+
import { indexLog, type LogIndex } from '../../kernel/log-index'
|
|
51
|
+
import {
|
|
52
|
+
dashboardFactsReducer,
|
|
53
|
+
producedAfter,
|
|
54
|
+
type DashboardFacts,
|
|
55
|
+
type PhaseTiming,
|
|
56
|
+
} from './facts'
|
|
46
57
|
import type { BuildRecord, StreamRecord } from '../../store/types'
|
|
47
58
|
import { reduceDispatchSettings } from '../../kernel/dispatch-settings'
|
|
48
59
|
import { projectSessions, type DashboardSession } from './detail'
|
|
@@ -386,99 +397,6 @@ function step(label: string, done: boolean, current: boolean, extra: StepExtra =
|
|
|
386
397
|
}
|
|
387
398
|
}
|
|
388
399
|
|
|
389
|
-
/** One phase occurrence's wall-clock span, keyed by phase. `startSeq` is the
|
|
390
|
-
* seq of the `*.started` event, so callers can scope by the same seq
|
|
391
|
-
* boundaries (`restartSince`/`cycleSince`) the step STATES already use. */
|
|
392
|
-
interface PhaseInterval {
|
|
393
|
-
start: number
|
|
394
|
-
end: number
|
|
395
|
-
startSeq: number
|
|
396
|
-
}
|
|
397
|
-
interface PhaseTiming {
|
|
398
|
-
closed: PhaseInterval[]
|
|
399
|
-
open?: { start: number; startSeq: number }
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Walk the raw event log into per-phase `[start, end]` intervals in epoch-ms
|
|
404
|
-
* (`Date.parse(ev.ts)`), the durations the reducer collapses away. Each
|
|
405
|
-
* `*.started` opens an interval for its phase key and its terminal event
|
|
406
|
-
* (`.completed`/`.verdict`) closes it. A second `*.started` while one is still
|
|
407
|
-
* open REPLACES the open start — a §15.6-C cross-sandbox re-run starts the
|
|
408
|
-
* phase afresh, so the crashed attempt contributes nothing. Finalize post-steps
|
|
409
|
-
* (`finalize.step-completed`) have no `.started` and so no interval.
|
|
410
|
-
*/
|
|
411
|
-
function phaseIntervals(events: AbEvent[]): Map<string, PhaseTiming> {
|
|
412
|
-
const timings = new Map<string, PhaseTiming>()
|
|
413
|
-
const get = (key: string): PhaseTiming => {
|
|
414
|
-
let t = timings.get(key)
|
|
415
|
-
if (t === undefined) {
|
|
416
|
-
t = { closed: [] }
|
|
417
|
-
timings.set(key, t)
|
|
418
|
-
}
|
|
419
|
-
return t
|
|
420
|
-
}
|
|
421
|
-
for (const ev of events) {
|
|
422
|
-
const ms = Date.parse(ev.ts)
|
|
423
|
-
const open = (key: string): void => {
|
|
424
|
-
get(key).open = { start: ms, startSeq: ev.seq }
|
|
425
|
-
}
|
|
426
|
-
const close = (key: string): void => {
|
|
427
|
-
const t = get(key)
|
|
428
|
-
if (t.open === undefined) return
|
|
429
|
-
t.closed.push({ start: t.open.start, end: ms, startSeq: t.open.startSeq })
|
|
430
|
-
t.open = undefined
|
|
431
|
-
}
|
|
432
|
-
switch (ev.type) {
|
|
433
|
-
case 'plan.started':
|
|
434
|
-
open('plan')
|
|
435
|
-
break
|
|
436
|
-
case 'plan.completed':
|
|
437
|
-
close('plan')
|
|
438
|
-
break
|
|
439
|
-
case 'plan-review.started':
|
|
440
|
-
open('plan-review')
|
|
441
|
-
break
|
|
442
|
-
case 'plan-review.verdict':
|
|
443
|
-
close('plan-review')
|
|
444
|
-
break
|
|
445
|
-
case 'implement.started':
|
|
446
|
-
open('implement')
|
|
447
|
-
break
|
|
448
|
-
case 'implement.completed':
|
|
449
|
-
close('implement')
|
|
450
|
-
break
|
|
451
|
-
case 'code-review.started':
|
|
452
|
-
open('code-review')
|
|
453
|
-
break
|
|
454
|
-
case 'code-review.verdict':
|
|
455
|
-
close('code-review')
|
|
456
|
-
break
|
|
457
|
-
case 'verify.started':
|
|
458
|
-
open(verifyPhase(ev.payload.step))
|
|
459
|
-
break
|
|
460
|
-
case 'verify.completed':
|
|
461
|
-
close(verifyPhase(ev.payload.step))
|
|
462
|
-
break
|
|
463
|
-
case 'finalize.started':
|
|
464
|
-
open('finalize')
|
|
465
|
-
break
|
|
466
|
-
case 'finalize.completed':
|
|
467
|
-
close('finalize')
|
|
468
|
-
break
|
|
469
|
-
case 'reconcile.started':
|
|
470
|
-
open('reconcile')
|
|
471
|
-
break
|
|
472
|
-
case 'reconcile.completed':
|
|
473
|
-
close('reconcile')
|
|
474
|
-
break
|
|
475
|
-
default:
|
|
476
|
-
break
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
return timings
|
|
480
|
-
}
|
|
481
|
-
|
|
482
400
|
/**
|
|
483
401
|
* Sum the in-scope occurrences of one phase into a `StepTiming`, or `undefined`
|
|
484
402
|
* when the phase never ran in scope (the step shows no time, AC 6). Scope is
|
|
@@ -489,12 +407,12 @@ function phaseIntervals(events: AbEvent[]): Map<string, PhaseTiming> {
|
|
|
489
407
|
* not advance (AC 10).
|
|
490
408
|
*/
|
|
491
409
|
function timingFor(
|
|
492
|
-
intervals:
|
|
410
|
+
intervals: Record<string, PhaseTiming>,
|
|
493
411
|
key: string,
|
|
494
412
|
sinceSeq: number,
|
|
495
413
|
frozenNow: number | undefined,
|
|
496
414
|
): StepTiming | undefined {
|
|
497
|
-
const t = intervals
|
|
415
|
+
const t = intervals[key]
|
|
498
416
|
if (t === undefined) return undefined
|
|
499
417
|
let accumulatedMs = 0
|
|
500
418
|
let inScope = false
|
|
@@ -532,8 +450,41 @@ export function projectBuild(
|
|
|
532
450
|
events: AbEvent[],
|
|
533
451
|
streams?: readonly StreamRecord[],
|
|
534
452
|
pinnedConfig?: Config,
|
|
453
|
+
): DashboardBuild | null {
|
|
454
|
+
return projectBuildFromFacts(
|
|
455
|
+
record,
|
|
456
|
+
state,
|
|
457
|
+
{
|
|
458
|
+
dashboard: dashboardFactsReducer.reduce(events),
|
|
459
|
+
log: indexLog(events),
|
|
460
|
+
deferral: currentDeferralObservationReducer.reduce(events),
|
|
461
|
+
},
|
|
462
|
+
config,
|
|
463
|
+
streams,
|
|
464
|
+
pinnedConfig,
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/** The reduced facts a row renders from, beside the build state: what
|
|
469
|
+
* `projectBuild` derives from the event array, advanced incrementally. */
|
|
470
|
+
export interface DashboardRowFacts {
|
|
471
|
+
dashboard: DashboardFacts
|
|
472
|
+
log: LogIndex
|
|
473
|
+
deferral: DeferralLedger
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/** `projectBuild` over already-reduced facts — the one implementation of the
|
|
477
|
+
* row projection, so a snapshot-backed row and a replayed row cannot differ. */
|
|
478
|
+
export function projectBuildFromFacts(
|
|
479
|
+
record: BuildRecord,
|
|
480
|
+
state: BuildState,
|
|
481
|
+
facts: DashboardRowFacts,
|
|
482
|
+
config: Config,
|
|
483
|
+
streams?: readonly StreamRecord[],
|
|
484
|
+
pinnedConfig?: Config,
|
|
535
485
|
): DashboardBuild | null {
|
|
536
486
|
const pipeline = pinnedConfig ?? config
|
|
487
|
+
const dashboard = facts.dashboard
|
|
537
488
|
const status = effectiveStatus(state)
|
|
538
489
|
if (!isVisible(status)) return null
|
|
539
490
|
const abortProgress =
|
|
@@ -546,35 +497,29 @@ export function projectBuild(
|
|
|
546
497
|
: undefined
|
|
547
498
|
|
|
548
499
|
if (status === 'queued') {
|
|
549
|
-
const created =
|
|
550
|
-
const workspace =
|
|
551
|
-
const released =
|
|
500
|
+
const created = dashboard.created
|
|
501
|
+
const workspace = dashboard.workspaceProvisioned
|
|
502
|
+
const released = dashboard.workspaceReleased
|
|
552
503
|
const openWorkspace =
|
|
553
|
-
workspace !== undefined && (released === undefined || workspace
|
|
554
|
-
const spec =
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
const commentPosted = events.findLast((event) => event.type === 'dispatch.comment-posted')
|
|
558
|
-
const latestFailure = events.findLast((event) => event.type === 'dispatch.failed')
|
|
504
|
+
workspace !== undefined && (released === undefined || workspace > released)
|
|
505
|
+
const spec = dashboard.specLanded
|
|
506
|
+
const commentPosted = dashboard.commentPosted
|
|
507
|
+
const latestFailure = dashboard.latestFailure
|
|
559
508
|
const failureSuperseded =
|
|
560
509
|
latestFailure !== undefined &&
|
|
561
|
-
((latestFailure.
|
|
562
|
-
|
|
563
|
-
created.seq > latestFailure.seq) ||
|
|
564
|
-
(latestFailure.payload.stage === 'workspace' &&
|
|
510
|
+
((latestFailure.stage === 'create' && created !== undefined && created > latestFailure.seq) ||
|
|
511
|
+
(latestFailure.stage === 'workspace' &&
|
|
565
512
|
workspace !== undefined &&
|
|
566
|
-
workspace
|
|
567
|
-
(latestFailure.
|
|
568
|
-
|
|
569
|
-
spec.seq > latestFailure.seq) ||
|
|
570
|
-
(latestFailure.payload.stage === 'comment' &&
|
|
513
|
+
workspace > latestFailure.seq) ||
|
|
514
|
+
(latestFailure.stage === 'spec' && spec !== undefined && spec > latestFailure.seq) ||
|
|
515
|
+
(latestFailure.stage === 'comment' &&
|
|
571
516
|
commentPosted !== undefined &&
|
|
572
|
-
commentPosted
|
|
517
|
+
commentPosted > latestFailure.seq))
|
|
573
518
|
const dispatch =
|
|
574
519
|
state.discardRequest !== undefined
|
|
575
520
|
? 'discard requested; cleanup and Ready handback pending'
|
|
576
521
|
: latestFailure !== undefined && !failureSuperseded
|
|
577
|
-
? `dispatch ${latestFailure.
|
|
522
|
+
? `dispatch ${latestFailure.stage} failed (attempt ${latestFailure.attempt}): ${latestFailure.error}`
|
|
578
523
|
: created === undefined
|
|
579
524
|
? 'dispatch initialization pending'
|
|
580
525
|
: !openWorkspace
|
|
@@ -591,7 +536,7 @@ export function projectBuild(
|
|
|
591
536
|
dispatch,
|
|
592
537
|
blockers: [],
|
|
593
538
|
autoMerge: autoMergeDisplay(state),
|
|
594
|
-
sessions: projectSessions(
|
|
539
|
+
sessions: projectSessions(dashboard.sessionEvents, streams),
|
|
595
540
|
}
|
|
596
541
|
}
|
|
597
542
|
|
|
@@ -612,7 +557,7 @@ export function projectBuild(
|
|
|
612
557
|
...(state.pr !== undefined && state.prState !== undefined
|
|
613
558
|
? { pr: { url: state.pr.url, state: state.prState } }
|
|
614
559
|
: {}),
|
|
615
|
-
sessions: projectSessions(
|
|
560
|
+
sessions: projectSessions(dashboard.sessionEvents, streams),
|
|
616
561
|
}
|
|
617
562
|
}
|
|
618
563
|
|
|
@@ -676,11 +621,8 @@ export function projectBuild(
|
|
|
676
621
|
// blocked build freezes at the latest durable event. Timers therefore do not
|
|
677
622
|
// advance behind either gate (AC 10). Each step scopes by the SAME seq boundary its state uses, so
|
|
678
623
|
// durations and states restart in lockstep (AC 12).
|
|
679
|
-
const intervals =
|
|
680
|
-
const pausedAt =
|
|
681
|
-
state.status === 'paused'
|
|
682
|
-
? events.findLast((event) => event.type === 'build.paused')?.ts
|
|
683
|
-
: undefined
|
|
624
|
+
const intervals = dashboard.intervals
|
|
625
|
+
const pausedAt = state.status === 'paused' ? dashboard.pausedAt : undefined
|
|
684
626
|
const frozenNow =
|
|
685
627
|
state.status === 'running'
|
|
686
628
|
? undefined
|
|
@@ -721,29 +663,25 @@ export function projectBuild(
|
|
|
721
663
|
// boundary. Starts and timing intervals are deliberately insufficient: a
|
|
722
664
|
// crashed phase has run, but has produced no output. Matching the round also
|
|
723
665
|
// drops a review verdict as soon as the next producer round starts.
|
|
724
|
-
const planProduced =
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
ev.payload.round === state.plan.round,
|
|
666
|
+
const planProduced = producedAfter(
|
|
667
|
+
dashboard.produced.planCompleted,
|
|
668
|
+
state.plan.round,
|
|
669
|
+
restartSince,
|
|
729
670
|
)
|
|
730
|
-
const planReviewProduced =
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
ev.payload.round === state.plan.round,
|
|
671
|
+
const planReviewProduced = producedAfter(
|
|
672
|
+
dashboard.produced.planReviewVerdict,
|
|
673
|
+
state.plan.round,
|
|
674
|
+
restartSince,
|
|
735
675
|
)
|
|
736
|
-
const implementProduced =
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
ev.payload.round === state.implement.round,
|
|
676
|
+
const implementProduced = producedAfter(
|
|
677
|
+
dashboard.produced.implementCompleted,
|
|
678
|
+
state.implement.round,
|
|
679
|
+
restartSince,
|
|
741
680
|
)
|
|
742
|
-
const codeReviewProduced =
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
ev.payload.round === state.implement.round,
|
|
681
|
+
const codeReviewProduced = producedAfter(
|
|
682
|
+
dashboard.produced.codeReviewVerdict,
|
|
683
|
+
state.implement.round,
|
|
684
|
+
restartSince,
|
|
747
685
|
)
|
|
748
686
|
|
|
749
687
|
// Finalize ran for the CURRENT spec — NOT `prState !== undefined`, which a
|
|
@@ -902,10 +840,10 @@ export function projectBuild(
|
|
|
902
840
|
: `exit status ${state.setupFailure.exitStatus}`
|
|
903
841
|
}): ${state.setupFailure.output || '(no output)'}`
|
|
904
842
|
|
|
905
|
-
const decision =
|
|
843
|
+
const decision = decideNextFromFacts(state, facts.log, pipeline)
|
|
906
844
|
const mergeWaitReason =
|
|
907
845
|
decision.kind === 'wait' && decision.reason === 'awaiting-pr'
|
|
908
|
-
?
|
|
846
|
+
? currentAutoMergeDeferralFrom(facts.deferral, state)
|
|
909
847
|
: undefined
|
|
910
848
|
|
|
911
849
|
return {
|
|
@@ -926,7 +864,7 @@ export function projectBuild(
|
|
|
926
864
|
...(state.pr !== undefined && state.prState !== undefined
|
|
927
865
|
? { pr: { url: state.pr.url, state: state.prState } }
|
|
928
866
|
: {}),
|
|
929
|
-
sessions: projectSessions(
|
|
867
|
+
sessions: projectSessions(dashboard.sessionEvents, streams),
|
|
930
868
|
}
|
|
931
869
|
}
|
|
932
870
|
|
|
@@ -2,7 +2,7 @@ import type { Config } from '../../config/schema'
|
|
|
2
2
|
import { composeBuildConfig } from '../../config/live'
|
|
3
3
|
import type { PipelineSourceMeta } from '../../config/pipeline-source'
|
|
4
4
|
import type { AbEvent } from '../../events/catalog'
|
|
5
|
-
import {
|
|
5
|
+
import { buildReducer, type BuildAcc, type BuildState } from '../../kernel/reducer'
|
|
6
6
|
import {
|
|
7
7
|
BUILD_EFFECTIVE_CONFIG_ARTIFACT,
|
|
8
8
|
parseBuildConfigMetadata,
|
|
@@ -38,6 +38,9 @@ export interface DashboardPollSnapshot {
|
|
|
38
38
|
interface LiveEntry {
|
|
39
39
|
kind: 'live'
|
|
40
40
|
events: AbEvent[]
|
|
41
|
+
/** The reducer accumulator `state` was derived from; the next delta is
|
|
42
|
+
* folded into a copy of it instead of re-reducing the whole log. */
|
|
43
|
+
acc: BuildAcc
|
|
41
44
|
state: BuildState
|
|
42
45
|
build: DashboardBuild | null
|
|
43
46
|
}
|
|
@@ -219,7 +222,8 @@ export class DashboardBuildPollCache {
|
|
|
219
222
|
}
|
|
220
223
|
|
|
221
224
|
const events = current === undefined ? delta : [...current.events, ...delta]
|
|
222
|
-
const
|
|
225
|
+
const acc = buildReducer.advance(current?.acc ?? buildReducer.initial(), delta)
|
|
226
|
+
const state = buildReducer.finish(acc)
|
|
223
227
|
if (isTerminal(state)) {
|
|
224
228
|
next.set(record.slug, { kind: 'terminal' })
|
|
225
229
|
continue
|
|
@@ -227,6 +231,7 @@ export class DashboardBuildPollCache {
|
|
|
227
231
|
next.set(record.slug, {
|
|
228
232
|
kind: 'live',
|
|
229
233
|
events,
|
|
234
|
+
acc,
|
|
230
235
|
state,
|
|
231
236
|
build: await this.projectRow(record, state, events, config),
|
|
232
237
|
})
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { distributionPath } from '../distribution'
|
|
2
2
|
import { DISPATCHER } from '../events/envelope'
|
|
3
|
-
import
|
|
3
|
+
import { hasOpenTick } from '../processes/tick-state'
|
|
4
4
|
import type { BuildStore } from '../store/types'
|
|
5
5
|
|
|
6
6
|
export const DISPATCH_CHILD_OPTIONS_ENV = 'AB_DISPATCH_CHILD_OPTIONS'
|
|
@@ -132,18 +132,6 @@ export interface DispatchChildSupervisorDeps {
|
|
|
132
132
|
}) => DispatchSubprocess
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
function hasOpenTick(events: readonly RepositoryEvent[], run: string): boolean {
|
|
136
|
-
const boundary = events.findLast(
|
|
137
|
-
(event) =>
|
|
138
|
-
'run' in event.payload &&
|
|
139
|
-
event.payload.run === run &&
|
|
140
|
-
(event.type === 'dispatcher.tick-started' ||
|
|
141
|
-
event.type === 'dispatcher.tick-completed' ||
|
|
142
|
-
event.type === 'dispatcher.tick-failed'),
|
|
143
|
-
)
|
|
144
|
-
return boundary?.type === 'dispatcher.tick-started'
|
|
145
|
-
}
|
|
146
|
-
|
|
147
135
|
function defaultSpawn(input: {
|
|
148
136
|
entrypoint: string
|
|
149
137
|
cwd: string
|