@defrex/autobuild 0.13.0 → 0.14.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 +19 -1
- package/bin/ab-dispatch-kernel.ts +4 -6
- package/docs/architecture.md +96 -70
- package/docs/hosted-service.md +2 -2
- package/docs/operator-api.md +1 -1
- package/docs/remote-store-protocol.md +84 -2
- package/package.json +4 -3
- package/packages/core/src/cli/attention.ts +18 -164
- package/packages/core/src/cli/build-control.ts +11 -15
- package/packages/core/src/cli/bulk-control.ts +3 -2
- package/packages/core/src/cli/context.ts +16 -78
- package/packages/core/src/cli/dashboard/model.ts +36 -22
- package/packages/core/src/cli/dispatch-frontend.ts +24 -67
- package/packages/core/src/cli/dispatch-process.ts +14 -7
- package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
- package/packages/core/src/cli/dispatch.ts +148 -79
- package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
- package/packages/core/src/cli/harvest.ts +71 -39
- package/packages/core/src/cli/historical-build-state.ts +27 -0
- package/packages/core/src/cli/main.ts +3 -3
- package/packages/core/src/cli/pr-attachments.ts +10 -13
- package/packages/core/src/cli/pr-summary-publication.ts +27 -60
- package/packages/core/src/cli/pr-summary.ts +43 -13
- package/packages/core/src/cli/publication-settlement.ts +10 -20
- package/packages/core/src/cli/repository-status.ts +38 -2
- package/packages/core/src/cli/status.ts +62 -32
- package/packages/core/src/cli/terminals.ts +25 -142
- package/packages/core/src/cli/ticket.ts +2 -2
- package/packages/core/src/cli/wait.ts +91 -84
- package/packages/core/src/cli/watch.ts +60 -97
- package/packages/core/src/config/pipeline-source.ts +4 -3
- package/packages/core/src/events/repository.ts +7 -3
- package/packages/core/src/integration/harness.ts +1 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2429 -9
- package/packages/core/src/kernel/auto-merge.ts +19 -3
- package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
- package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
- package/packages/core/src/kernel/harvest.ts +25 -23
- package/packages/core/src/kernel/log-index-value.ts +38 -0
- package/packages/core/src/kernel/read-facts.ts +637 -0
- package/packages/core/src/kernel/reducer-registry.ts +137 -3
- package/packages/core/src/kernel/repository-attention.ts +179 -0
- package/packages/core/src/kernel/service-facts.ts +135 -0
- package/packages/core/src/kernel/terminal-guard.ts +133 -0
- package/packages/core/src/operator/control.ts +5 -6
- package/packages/core/src/operator/index.ts +2 -0
- package/packages/core/src/operator/query.ts +37 -20
- package/packages/core/src/operator/read-view.ts +4 -13
- package/packages/core/src/operator/requests.ts +2 -14
- package/packages/core/src/operator/sandbox.ts +10 -9
- package/packages/core/src/operator/tickets.ts +2 -2
- package/packages/core/src/plugin-sdk/index.ts +13 -0
- package/packages/core/src/processes/build-child.ts +6 -6
- package/packages/core/src/processes/build-runner.ts +62 -70
- package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
- package/packages/core/src/processes/dispatcher.ts +113 -173
- package/packages/core/src/processes/harvest-execution-state.ts +15 -9
- package/packages/core/src/processes/harvest-runner.ts +34 -70
- package/packages/core/src/processes/harvest.ts +38 -15
- package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
- package/packages/core/src/processes/orchestrator-tick.ts +2 -2
- package/packages/core/src/processes/publication-loss.ts +35 -21
- package/packages/core/src/processes/repo-view.ts +229 -1134
- package/packages/core/src/processes/tick-publication.ts +110 -0
- package/packages/core/src/store/adapter.ts +23 -0
- package/packages/core/src/store/build-scope.ts +17 -0
- package/packages/core/src/store/contract.ts +251 -0
- package/packages/core/src/store/local/schema.ts +1 -0
- package/packages/core/src/store/local/store.ts +179 -54
- package/packages/core/src/store/memory.ts +119 -29
- package/packages/core/src/store/phase-session.ts +17 -0
- package/packages/core/src/store/reduced-reads.ts +239 -0
- package/packages/core/src/store/reducer-value-shapes.json +14056 -0
- package/packages/core/src/store/reducer-value-validation.ts +96 -0
- package/packages/core/src/store/reducer-values.ts +145 -0
- package/packages/core/src/store/remote/client.ts +41 -0
- package/packages/core/src/store/remote/compatibility.ts +2 -0
- package/packages/core/src/store/remote/index.ts +19 -0
- package/packages/core/src/store/remote/protocol.ts +16 -0
- package/packages/core/src/store/remote/version.ts +3 -1
- package/packages/core/src/store/repo-state-events.ts +1 -1
- package/packages/core/src/store/repo-tick.ts +72 -0
- package/packages/core/src/store/session-handle.ts +19 -0
- package/packages/core/src/store/snapshots.ts +10 -0
- package/packages/core/src/store/ticket-assets.ts +4 -4
- package/packages/core/src/store/types.ts +39 -2
- package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
- package/packages/core/src/testing/index.ts +8 -0
- package/packages/core/src/testing/reduced-read-budget.ts +462 -0
- package/skills/guide/SKILL.md +6 -1
- package/skills/guide/references/operator-api.md +1 -1
- package/skills/guide/references/remote-store-protocol.md +84 -2
- package/templates/hosted-service/package.json +2 -2
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { readBuildDigests } from '../store/reduced-reads'
|
|
2
|
+
import { readBuildStreamSeed } from '../store/reduced-reads'
|
|
3
|
+
import { buildStreamSeedReducer, type BuildStreamSeed } from '../kernel/read-facts'
|
|
4
|
+
import { historicalBuildState } from './historical-build-state'
|
|
5
|
+
import { currentDeferralObservationReducer, type DeferralLedger } from '../kernel/auto-merge'
|
|
1
6
|
/**
|
|
2
7
|
* `ab wait` — the blocking sibling of `ab watch` (§8.2, §7.2). It follows the
|
|
3
8
|
* event logs of one, several, or all active builds in the repository and
|
|
@@ -31,11 +36,11 @@
|
|
|
31
36
|
* Read-only: it appends no event, claims or renews no lease, and creates no
|
|
32
37
|
* build or repository record. Output carries no ANSI, ever, in either form.
|
|
33
38
|
*/
|
|
34
|
-
import {
|
|
39
|
+
import { mergeDecisionFromFacts, mergeIdentity } from './attention'
|
|
35
40
|
import type { AbEvent } from '../events/catalog'
|
|
36
41
|
import type { EventType } from '../events/payloads'
|
|
37
42
|
import type { BuildState, PrLifecycle } from '../kernel/reducer'
|
|
38
|
-
import { reduceBuild } from '../kernel/reducer'
|
|
43
|
+
import { buildReducer, reduceBuild } from '../kernel/reducer'
|
|
39
44
|
import { isPhase, type BuildStatus, type Phase } from '../ontology'
|
|
40
45
|
import type { Exec } from '../ports/workspace/git-worktree'
|
|
41
46
|
import {
|
|
@@ -156,8 +161,25 @@ export function parseConditions(
|
|
|
156
161
|
return { conditions, globs }
|
|
157
162
|
}
|
|
158
163
|
|
|
164
|
+
function attentionFrom(
|
|
165
|
+
input: AbEvent[] | { state: BuildState; deferral: DeferralLedger },
|
|
166
|
+
state?: BuildState,
|
|
167
|
+
) {
|
|
168
|
+
return Array.isArray(input)
|
|
169
|
+
? mergeDecisionFromFacts(
|
|
170
|
+
state ?? reduceBuild(input),
|
|
171
|
+
currentDeferralObservationReducer.reduce(input),
|
|
172
|
+
new Date(0),
|
|
173
|
+
)
|
|
174
|
+
: mergeDecisionFromFacts(input.state, input.deferral, new Date(0))
|
|
175
|
+
}
|
|
176
|
+
|
|
159
177
|
/** Does one condition body hold over a reduction? Attention includes current escalation and merge state. */
|
|
160
|
-
function stateSatisfies(
|
|
178
|
+
function stateSatisfies(
|
|
179
|
+
cond: ConditionBody,
|
|
180
|
+
state: BuildState,
|
|
181
|
+
events: AbEvent[] | { state: BuildState; deferral: DeferralLedger } = [],
|
|
182
|
+
): boolean {
|
|
161
183
|
switch (cond.kind) {
|
|
162
184
|
case 'status':
|
|
163
185
|
return state.status === cond.status
|
|
@@ -168,7 +190,7 @@ function stateSatisfies(cond: ConditionBody, state: BuildState, events: AbEvent[
|
|
|
168
190
|
case 'phase':
|
|
169
191
|
return (state.phase ?? null) === cond.phase
|
|
170
192
|
case 'attention':
|
|
171
|
-
return
|
|
193
|
+
return (state.openEscalations?.length ?? 0) > 0 || attentionFrom(events, state) !== null
|
|
172
194
|
}
|
|
173
195
|
}
|
|
174
196
|
|
|
@@ -180,7 +202,7 @@ function stateSatisfies(cond: ConditionBody, state: BuildState, events: AbEvent[
|
|
|
180
202
|
export function matchState(
|
|
181
203
|
conditions: readonly WaitCondition[],
|
|
182
204
|
state: BuildState,
|
|
183
|
-
events: AbEvent[] = [],
|
|
205
|
+
events: AbEvent[] | { state: BuildState; deferral: DeferralLedger } = [],
|
|
184
206
|
): string | null {
|
|
185
207
|
for (const { text, cond } of conditions) {
|
|
186
208
|
if (stateSatisfies(cond, state, events)) return text
|
|
@@ -195,13 +217,12 @@ export function matchEvent(
|
|
|
195
217
|
event: Pick<AbEvent, 'type'>,
|
|
196
218
|
conditions: readonly WaitCondition[],
|
|
197
219
|
globs: readonly EventGlob[],
|
|
198
|
-
events: AbEvent[] = [],
|
|
220
|
+
events: AbEvent[] | { state: BuildState; deferral: DeferralLedger } = [],
|
|
199
221
|
): string | null {
|
|
200
222
|
for (const { text, cond } of conditions) {
|
|
201
223
|
if (
|
|
202
224
|
cond.kind === 'attention' &&
|
|
203
|
-
(isAttentionEvent(event.type) ||
|
|
204
|
-
(events.length > 0 && mergeDecision(events, new Date(0)) !== null))
|
|
225
|
+
(isAttentionEvent(event.type) || attentionFrom(events) !== null)
|
|
205
226
|
)
|
|
206
227
|
return text
|
|
207
228
|
}
|
|
@@ -222,14 +243,13 @@ function matchFirst(
|
|
|
222
243
|
state: BuildState,
|
|
223
244
|
conditions: readonly WaitCondition[],
|
|
224
245
|
globs: readonly EventGlob[],
|
|
225
|
-
events: AbEvent[] = [],
|
|
246
|
+
events: AbEvent[] | { state: BuildState; deferral: DeferralLedger } = [],
|
|
226
247
|
): string | null {
|
|
227
248
|
for (const { text, cond } of conditions) {
|
|
228
249
|
if (stateSatisfies(cond, state, events)) return text
|
|
229
250
|
if (
|
|
230
251
|
cond.kind === 'attention' &&
|
|
231
|
-
(isAttentionEvent(event.type) ||
|
|
232
|
-
(events.length > 0 && mergeDecision(events, new Date(0)) !== null))
|
|
252
|
+
(isAttentionEvent(event.type) || attentionFrom(events) !== null)
|
|
233
253
|
)
|
|
234
254
|
return text
|
|
235
255
|
}
|
|
@@ -276,7 +296,7 @@ interface WaitStream {
|
|
|
276
296
|
slug: string
|
|
277
297
|
named: boolean
|
|
278
298
|
/** The full prefix, appended in seq order — what `reduceBuild` runs over. */
|
|
279
|
-
|
|
299
|
+
seed: BuildStreamSeed
|
|
280
300
|
lastSeq: number
|
|
281
301
|
status: BuildStatus
|
|
282
302
|
}
|
|
@@ -357,11 +377,7 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
357
377
|
const finish = (condition: string | null, event: AbEvent, state: BuildState): number => {
|
|
358
378
|
const observation =
|
|
359
379
|
condition === 'attention' && event.type !== 'escalation.raised'
|
|
360
|
-
?
|
|
361
|
-
streams.get(event.build)?.events.filter((e) => e.seq <= event.seq) ?? [],
|
|
362
|
-
now(),
|
|
363
|
-
state,
|
|
364
|
-
)
|
|
380
|
+
? mergeDecisionFromFacts(state, streams.get(event.build)!.seed.deferral, now())
|
|
365
381
|
: null
|
|
366
382
|
if (observation) deliveredAttention.add(mergeIdentity(event.build, observation.pr.number))
|
|
367
383
|
const record = {
|
|
@@ -394,22 +410,30 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
394
410
|
if (satisfied === null) satisfied = { event, state, condition }
|
|
395
411
|
}
|
|
396
412
|
|
|
397
|
-
const eventConditions = (event: AbEvent,
|
|
413
|
+
const eventConditions = (event: AbEvent, seed: BuildStreamSeed): readonly WaitCondition[] => {
|
|
398
414
|
if (event.type === 'escalation.raised') return conditions
|
|
399
|
-
const observation =
|
|
415
|
+
const observation = mergeDecisionFromFacts(
|
|
416
|
+
buildReducer.finish(seed.build),
|
|
417
|
+
seed.deferral,
|
|
418
|
+
now(),
|
|
419
|
+
)
|
|
400
420
|
return observation &&
|
|
401
421
|
!deliveredAttention.has(mergeIdentity(event.build, observation.pr.number))
|
|
402
422
|
? conditions
|
|
403
423
|
: conditions.filter(({ cond }) => cond.kind !== 'attention')
|
|
404
424
|
}
|
|
405
425
|
|
|
406
|
-
const registerStream = (
|
|
407
|
-
|
|
426
|
+
const registerStream = (
|
|
427
|
+
slug: string,
|
|
428
|
+
current: { value: BuildStreamSeed; cursor: number },
|
|
429
|
+
named: boolean,
|
|
430
|
+
): WaitStream => {
|
|
431
|
+
const state = buildReducer.finish(current.value.build)
|
|
408
432
|
const stream: WaitStream = {
|
|
409
433
|
slug,
|
|
410
434
|
named,
|
|
411
|
-
|
|
412
|
-
lastSeq:
|
|
435
|
+
seed: current.value,
|
|
436
|
+
lastSeq: current.cursor,
|
|
413
437
|
status: state.status,
|
|
414
438
|
}
|
|
415
439
|
streams.set(slug, stream)
|
|
@@ -421,10 +445,10 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
421
445
|
* already satisfying when it becomes targeted ends the wait with its
|
|
422
446
|
* latest event. Not cursor-relative — `--since` does not suppress it. */
|
|
423
447
|
const checkRegistrationState = (stream: WaitStream): void => {
|
|
424
|
-
const state =
|
|
425
|
-
const condition = matchState(conditions, state, stream.
|
|
426
|
-
if (condition !== null &&
|
|
427
|
-
match(
|
|
448
|
+
const state = buildReducer.finish(stream.seed.build)
|
|
449
|
+
const condition = matchState(conditions, state, { state, deferral: stream.seed.deferral })
|
|
450
|
+
if (condition !== null && state.lastEvent !== undefined) {
|
|
451
|
+
match(state.lastEvent!, state, condition)
|
|
428
452
|
}
|
|
429
453
|
}
|
|
430
454
|
/**
|
|
@@ -438,42 +462,43 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
438
462
|
if (streams.has(slug)) return
|
|
439
463
|
const record = await store.getBuild(slug)
|
|
440
464
|
if (record === null) return
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// reduction (approvals, findings, answered escalations, PR facts,
|
|
444
|
-
// observations), so bounding it would need reducer-partition store
|
|
445
|
-
// machinery out of AUT-545's scope. Required for `--since` replay and
|
|
446
|
-
// the registration state check over the full reduction.
|
|
447
|
-
const events = await store.getEvents(slug)
|
|
448
|
-
const stream = registerStream(slug, events, slugs.includes(slug))
|
|
465
|
+
const current = await readBuildStreamSeed(store, slug)
|
|
466
|
+
const stream = registerStream(slug, current, slugs.includes(slug))
|
|
449
467
|
if (resumeSeq !== undefined) {
|
|
468
|
+
stream.seed = await historicalBuildState(store, slug, resumeSeq, current)
|
|
450
469
|
stream.lastSeq = resumeSeq
|
|
451
470
|
positions[slug] = resumeSeq
|
|
452
|
-
// Replay the backlog into a scratch prefix: each evaluated state is
|
|
453
|
-
// the reduction of the prefix ENDING at that event, never the
|
|
454
|
-
// reduction of the full loaded prefix.
|
|
455
|
-
const replay: AbEvent[] = events.filter((event) => event.seq <= resumeSeq)
|
|
456
471
|
if (resumedCursor?.deliveredAttention === undefined) {
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
historical.push(event)
|
|
460
|
-
const observation = mergeDecision(historical, now())
|
|
461
|
-
if (observation) deliveredAttention.add(mergeIdentity(slug, observation.pr.number))
|
|
462
|
-
}
|
|
472
|
+
for (const pr of stream.seed.mergeAttentionPrs)
|
|
473
|
+
deliveredAttention.add(mergeIdentity(slug, pr))
|
|
463
474
|
}
|
|
464
|
-
for (const event of
|
|
465
|
-
|
|
466
|
-
replay.push(event)
|
|
475
|
+
for (const event of await store.getEvents(slug, resumeSeq)) {
|
|
476
|
+
stream.seed = buildStreamSeedReducer.advance(stream.seed, [event])
|
|
467
477
|
stream.lastSeq = event.seq
|
|
468
478
|
positions[slug] = event.seq
|
|
469
|
-
const
|
|
479
|
+
const state = buildReducer.finish(stream.seed.build)
|
|
480
|
+
const condition = matchEvent(event, eventConditions(event, stream.seed), globs, {
|
|
481
|
+
state,
|
|
482
|
+
deferral: stream.seed.deferral,
|
|
483
|
+
})
|
|
470
484
|
if (condition !== null) {
|
|
471
|
-
match(event,
|
|
485
|
+
match(event, state, condition)
|
|
472
486
|
break
|
|
473
487
|
}
|
|
474
488
|
}
|
|
475
489
|
}
|
|
476
|
-
if (satisfied === null)
|
|
490
|
+
if (satisfied === null) {
|
|
491
|
+
// Registration checks current state independently from historical replay.
|
|
492
|
+
const latest = buildReducer.finish(current.value.build)
|
|
493
|
+
const condition = matchState(conditions, latest, {
|
|
494
|
+
state: latest,
|
|
495
|
+
deferral: current.value.deferral,
|
|
496
|
+
})
|
|
497
|
+
if (condition !== null && latest.lastEvent) {
|
|
498
|
+
stream.seed = current.value
|
|
499
|
+
match(latest.lastEvent, latest, condition)
|
|
500
|
+
}
|
|
501
|
+
}
|
|
477
502
|
}
|
|
478
503
|
|
|
479
504
|
/** Builds discovery read in full and found terminal; never re-read. */
|
|
@@ -488,33 +513,20 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
488
513
|
* build to appear.
|
|
489
514
|
*/
|
|
490
515
|
const discoverBuilds = async (): Promise<void> => {
|
|
491
|
-
|
|
492
|
-
// one flat batch read replaces a full getEvents per candidate build, per
|
|
493
|
-
// discovery pass; `terminal` is reduceBuild's exact terminal rule
|
|
494
|
-
// (store/digest.ts, pinned by contract). The per-build full read below
|
|
495
|
-
// remains deliberate for builds the digest cannot retire: its retained
|
|
496
|
-
// prefix is consumed at full replay depth by every later reduction
|
|
497
|
-
// (approvals, findings, answered escalations, PR facts, observations), so
|
|
498
|
-
// bounding it would need reducer-partition store machinery out of
|
|
499
|
-
// AUT-545's scope. The reduceBuild re-check stays: it is the
|
|
500
|
-
// legacy-record fallback (a record whose `repo` predates origin
|
|
501
|
-
// normalization is in `mine` via `repoOrigin` but absent from the digest
|
|
502
|
-
// map, which keys on `record.repo`) and it keeps this filter unable to
|
|
503
|
-
// diverge from reduceBuild.
|
|
504
|
-
const digests = await store.getRepoBuildDigests(repo)
|
|
516
|
+
const digests = await readBuildDigests(store, repo)
|
|
505
517
|
for (const record of (await store.listBuilds()).filter(mine)) {
|
|
506
518
|
if (streams.has(record.slug) || retired.has(record.slug)) continue
|
|
507
519
|
const digest = digests.get(record.slug)
|
|
508
520
|
if (digest !== undefined && digest.terminal !== undefined) continue
|
|
509
|
-
const
|
|
510
|
-
const status =
|
|
521
|
+
const current = await readBuildStreamSeed(store, record.slug)
|
|
522
|
+
const status = buildReducer.finish(current.value.build).status
|
|
511
523
|
if (!NONTERMINAL_STATUSES.includes(status)) {
|
|
512
524
|
// Terminal is permanent (reduceBuild never clears it): a legacy
|
|
513
525
|
// record the digest cannot retire is read once, not every pass.
|
|
514
526
|
retired.add(record.slug)
|
|
515
527
|
continue
|
|
516
528
|
}
|
|
517
|
-
const stream = registerStream(record.slug,
|
|
529
|
+
const stream = registerStream(record.slug, current, false)
|
|
518
530
|
if (satisfied === null) checkRegistrationState(stream)
|
|
519
531
|
}
|
|
520
532
|
}
|
|
@@ -525,18 +537,15 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
525
537
|
): Promise<void> => {
|
|
526
538
|
const fresh = await store.getEvents(stream.slug, stream.lastSeq, readOpts)
|
|
527
539
|
for (const event of fresh) {
|
|
528
|
-
stream.
|
|
540
|
+
stream.seed = buildStreamSeedReducer.advance(stream.seed, [event])
|
|
529
541
|
stream.lastSeq = event.seq
|
|
530
542
|
positions[stream.slug] = event.seq
|
|
531
|
-
const state =
|
|
543
|
+
const state = buildReducer.finish(stream.seed.build)
|
|
532
544
|
stream.status = state.status
|
|
533
|
-
const condition = matchFirst(
|
|
534
|
-
event,
|
|
545
|
+
const condition = matchFirst(event, state, eventConditions(event, stream.seed), globs, {
|
|
535
546
|
state,
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
stream.events,
|
|
539
|
-
)
|
|
547
|
+
deferral: stream.seed.deferral,
|
|
548
|
+
})
|
|
540
549
|
if (condition !== null) {
|
|
541
550
|
match(event, state, condition)
|
|
542
551
|
return
|
|
@@ -556,21 +565,19 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
556
565
|
/** The exit-2 record: among the named builds' terminal events, the one
|
|
557
566
|
* with the latest `ts` (ties broken by slug order) — the event that made
|
|
558
567
|
* the final named build terminal. */
|
|
559
|
-
const pickTerminalRecord = (): { event: AbEvent; state: BuildState } => {
|
|
568
|
+
const pickTerminalRecord = async (): Promise<{ event: AbEvent; state: BuildState }> => {
|
|
560
569
|
let best: { event: AbEvent; state: BuildState; slug: string } | null = null
|
|
561
570
|
for (const slug of slugs) {
|
|
562
571
|
const stream = streams.get(slug)
|
|
563
572
|
if (stream === undefined) continue
|
|
564
|
-
const terminalEvent =
|
|
565
|
-
.reverse()
|
|
566
|
-
.find((event) => event.type === 'build.completed' || event.type === 'build.aborted')
|
|
573
|
+
const terminalEvent = stream.seed.terminalEvent
|
|
567
574
|
if (terminalEvent === undefined) continue
|
|
568
575
|
if (
|
|
569
576
|
best === null ||
|
|
570
577
|
terminalEvent.ts > best.event.ts ||
|
|
571
578
|
(terminalEvent.ts === best.event.ts && slug < best.slug)
|
|
572
579
|
) {
|
|
573
|
-
best = { event: terminalEvent, state:
|
|
580
|
+
best = { event: terminalEvent, state: buildReducer.finish(stream.seed.build), slug }
|
|
574
581
|
}
|
|
575
582
|
}
|
|
576
583
|
// A named build is terminal only through its terminal event, so
|
|
@@ -647,7 +654,7 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
647
654
|
} else if (namedAllTerminal()) {
|
|
648
655
|
// Decision 5, start form: every named build is already terminal and
|
|
649
656
|
// no condition held — show how the build ended.
|
|
650
|
-
const terminal = pickTerminalRecord()
|
|
657
|
+
const terminal = await pickTerminalRecord()
|
|
651
658
|
return finish(null, terminal.event, terminal.state)
|
|
652
659
|
}
|
|
653
660
|
|
|
@@ -664,7 +671,7 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
664
671
|
if (aborted()) break
|
|
665
672
|
await tick()
|
|
666
673
|
if (!hasMatch() && namedAllTerminal()) {
|
|
667
|
-
const terminal = pickTerminalRecord()
|
|
674
|
+
const terminal = await pickTerminalRecord()
|
|
668
675
|
return finish(null, terminal.event, terminal.state)
|
|
669
676
|
}
|
|
670
677
|
}
|
|
@@ -752,7 +759,7 @@ export async function abWait(opts: AbWaitOpts): Promise<number> {
|
|
|
752
759
|
// conditions live — so after the quiesce drain this decision re-reads
|
|
753
760
|
// the same live predicates (`hasMatch`, `namedAllTerminal`, `aborted`,
|
|
754
761
|
// the deadline) to pick the exit.
|
|
755
|
-
const terminal = pickTerminalRecord()
|
|
762
|
+
const terminal = await pickTerminalRecord()
|
|
756
763
|
return finish(null, terminal.event, terminal.state)
|
|
757
764
|
}
|
|
758
765
|
if (aborted()) {
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { readBuildDigests } from '../store/reduced-reads'
|
|
2
|
+
import { readBuildStreamSeed, readRepositoryAttention } from '../store/reduced-reads'
|
|
3
|
+
import { buildStreamSeedReducer, type BuildStreamSeed } from '../kernel/read-facts'
|
|
4
|
+
import { historicalBuildState, historicalRepositoryAttention } from './historical-build-state'
|
|
1
5
|
import { readDispatcherLiveness } from './dispatcher-liveness'
|
|
2
6
|
import { projectDispatcherLiveness } from '../kernel/dispatcher-liveness'
|
|
3
7
|
import type { RepositoryRecord } from '../store/types'
|
|
@@ -44,7 +48,7 @@ import type { AbEvent } from '../events/catalog'
|
|
|
44
48
|
import type { EventType } from '../events/payloads'
|
|
45
49
|
import type { RepositoryEvent } from '../events/repository'
|
|
46
50
|
import {
|
|
47
|
-
|
|
51
|
+
mergeDecisionFromFacts,
|
|
48
52
|
mergeIdentity,
|
|
49
53
|
RepositoryAttention,
|
|
50
54
|
repositoryNeedsAttention,
|
|
@@ -52,7 +56,7 @@ import {
|
|
|
52
56
|
type PersistenceObservation,
|
|
53
57
|
} from './attention'
|
|
54
58
|
import type { BuildState, PrLifecycle } from '../kernel/reducer'
|
|
55
|
-
import {
|
|
59
|
+
import { buildReducer } from '../kernel/reducer'
|
|
56
60
|
import type { BuildOutcome, BuildStatus, Phase } from '../ontology'
|
|
57
61
|
import type { Exec } from '../ports/workspace/git-worktree'
|
|
58
62
|
import { PhaseSessionError } from '../store/phase-session'
|
|
@@ -516,7 +520,7 @@ interface BuildStream {
|
|
|
516
520
|
slug: string
|
|
517
521
|
named: boolean
|
|
518
522
|
/** The full prefix, appended in seq order — what `reduceBuild` runs over. */
|
|
519
|
-
|
|
523
|
+
seed: BuildStreamSeed
|
|
520
524
|
lastSeq: number
|
|
521
525
|
status: BuildStatus
|
|
522
526
|
}
|
|
@@ -597,11 +601,9 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
597
601
|
? decodeCursor(opts.since, { store: storeRef, repo })
|
|
598
602
|
: undefined
|
|
599
603
|
const deliveredAttention = new Set(resumedCursor?.deliveredAttention ?? [])
|
|
600
|
-
|
|
604
|
+
let repositoryAttention = new RepositoryAttention()
|
|
601
605
|
let attentionInitialized = false
|
|
602
606
|
let livenessEvents: RepositoryEvent[] = []
|
|
603
|
-
let livenessSeq = 0
|
|
604
|
-
let livenessInitialized = false
|
|
605
607
|
const observedOwners = new Map<string, RepositoryRecord>()
|
|
606
608
|
const streams = new Map<string, Stream>()
|
|
607
609
|
// Every stream's position — matching or not, delivered or baselined —
|
|
@@ -674,29 +676,14 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
674
676
|
if (stop) return
|
|
675
677
|
}
|
|
676
678
|
}
|
|
677
|
-
const baselineMerge = (slug: string,
|
|
678
|
-
|
|
679
|
-
// historical prefix, even if consent was subsequently armed again.
|
|
680
|
-
const prefix: AbEvent[] = []
|
|
681
|
-
for (const event of events) {
|
|
682
|
-
prefix.push(event)
|
|
683
|
-
const observation = mergeDecision(prefix, now())
|
|
684
|
-
if (observation) deliveredAttention.add(mergeIdentity(slug, observation.pr.number))
|
|
685
|
-
}
|
|
679
|
+
const baselineMerge = (slug: string, seed: BuildStreamSeed): void => {
|
|
680
|
+
for (const pr of seed.mergeAttentionPrs) deliveredAttention.add(mergeIdentity(slug, pr))
|
|
686
681
|
}
|
|
687
682
|
|
|
688
683
|
const checkLiveness = async (): Promise<void> => {
|
|
689
684
|
if (!followRepository || stop) return
|
|
690
|
-
if (!livenessInitialized) {
|
|
691
|
-
const input = await readDispatcherLiveness(store, repo, now())
|
|
692
|
-
livenessEvents = input.events
|
|
693
|
-
livenessSeq = input.events.at(-1)?.seq ?? 0
|
|
694
|
-
livenessInitialized = true
|
|
695
|
-
}
|
|
696
685
|
if ((await store.getRepo(repo)) === null) return
|
|
697
|
-
|
|
698
|
-
livenessEvents.push(...fresh)
|
|
699
|
-
livenessSeq = fresh.at(-1)?.seq ?? livenessSeq
|
|
686
|
+
livenessEvents = (await readDispatcherLiveness(store, repo, now())).events
|
|
700
687
|
// Sample metadata after the journal. Recheck both immediately before
|
|
701
688
|
// reporting: a failed read is infrastructure, never evidence of death.
|
|
702
689
|
const record = await store.getRepo(repo)
|
|
@@ -709,9 +696,7 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
709
696
|
Date.parse(owner.lease.expiresAt) > now().getTime()
|
|
710
697
|
)
|
|
711
698
|
continue
|
|
712
|
-
|
|
713
|
-
livenessEvents.push(...delta)
|
|
714
|
-
livenessSeq = delta.at(-1)?.seq ?? livenessSeq
|
|
699
|
+
livenessEvents = (await readDispatcherLiveness(store, repo, now())).events
|
|
715
700
|
const current = await store.getRepo(repo)
|
|
716
701
|
if (current?.lease?.holder === id) {
|
|
717
702
|
observedOwners.set(id, current)
|
|
@@ -758,14 +743,16 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
758
743
|
* stream's just-extended array; the resume path replays the backlog
|
|
759
744
|
* into a scratch prefix so a resumed record never leaks state from
|
|
760
745
|
* events that landed after it. */
|
|
761
|
-
const processBuildEvent = (stream: BuildStream, event: AbEvent
|
|
746
|
+
const processBuildEvent = (stream: BuildStream, event: AbEvent): void => {
|
|
762
747
|
if (stop) return
|
|
763
748
|
stream.lastSeq = event.seq
|
|
764
749
|
positions[stream.slug] = event.seq
|
|
765
|
-
|
|
750
|
+
stream.seed = buildStreamSeedReducer.advance(stream.seed, [event])
|
|
751
|
+
const state = buildReducer.finish(stream.seed.build)
|
|
752
|
+
stream.status = state.status
|
|
766
753
|
if (matchesBuildFilter(event.type, filters)) emit(event, state)
|
|
767
754
|
if (!filters.length && !stop) {
|
|
768
|
-
const observation =
|
|
755
|
+
const observation = mergeDecisionFromFacts(state, stream.seed.deferral, now())
|
|
769
756
|
if (observation)
|
|
770
757
|
emitObservation(
|
|
771
758
|
observation,
|
|
@@ -799,44 +786,28 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
799
786
|
if (streams.has(slug)) return true
|
|
800
787
|
const record = await store.getBuild(slug)
|
|
801
788
|
if (record === null) return false
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
// the baselined stream prefix. Mirrors cli/wait.ts trackBuild.
|
|
808
|
-
const events = await store.getEvents(slug)
|
|
789
|
+
const current = await readBuildStreamSeed(store, slug)
|
|
790
|
+
const seed =
|
|
791
|
+
resumeSeq === undefined
|
|
792
|
+
? current.value
|
|
793
|
+
: await historicalBuildState(store, slug, resumeSeq, current)
|
|
809
794
|
const stream: BuildStream = {
|
|
810
795
|
kind: 'build',
|
|
811
796
|
slug,
|
|
812
797
|
named: slugs.includes(slug),
|
|
813
|
-
|
|
814
|
-
lastSeq:
|
|
815
|
-
status:
|
|
798
|
+
seed,
|
|
799
|
+
lastSeq: resumeSeq ?? current.cursor,
|
|
800
|
+
status: buildReducer.finish(current.value.build).status,
|
|
816
801
|
}
|
|
817
802
|
streams.set(slug, stream)
|
|
818
803
|
positions[slug] = stream.lastSeq
|
|
819
|
-
if (resumeSeq === undefined)
|
|
820
|
-
baselineMerge(slug,
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
slug,
|
|
827
|
-
events.filter((event) => event.seq <= resumeSeq),
|
|
828
|
-
)
|
|
829
|
-
stream.lastSeq = resumeSeq
|
|
830
|
-
// Replay the backlog into a scratch prefix: each record's state is
|
|
831
|
-
// the reduction of the prefix ENDING at that event, never the
|
|
832
|
-
// reduction of the full loaded prefix, which would leak state from
|
|
833
|
-
// events that landed after the one being recorded.
|
|
834
|
-
const replay: AbEvent[] = events.filter((event) => event.seq <= resumeSeq)
|
|
835
|
-
for (const event of events) {
|
|
836
|
-
if (event.seq <= resumeSeq) continue
|
|
837
|
-
replay.push(event)
|
|
838
|
-
processBuildEvent(stream, event, replay)
|
|
839
|
-
if (stop) break
|
|
804
|
+
if (resumeSeq === undefined || resumedCursor?.deliveredAttention === undefined)
|
|
805
|
+
baselineMerge(slug, seed)
|
|
806
|
+
if (resumeSeq !== undefined) {
|
|
807
|
+
for (const event of await store.getEvents(slug, resumeSeq)) {
|
|
808
|
+
processBuildEvent(stream, event)
|
|
809
|
+
if (stop) break
|
|
810
|
+
}
|
|
840
811
|
}
|
|
841
812
|
return true
|
|
842
813
|
}
|
|
@@ -856,26 +827,13 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
856
827
|
* the throw stay tracked, so the retry neither duplicates nor skips.
|
|
857
828
|
*/
|
|
858
829
|
const discoverBuilds = async (): Promise<void> => {
|
|
859
|
-
|
|
860
|
-
// one flat batch read replaces a full getEvents per candidate build, per
|
|
861
|
-
// discovery pass; `terminal` is reduceBuild's exact terminal rule
|
|
862
|
-
// (store/digest's reduceBuildDigest, pinned by contract). The per-build
|
|
863
|
-
// full read below remains deliberate for builds the digest cannot retire:
|
|
864
|
-
// its retained prefix is consumed at full replay depth by every later
|
|
865
|
-
// reduction (approvals, findings, answered escalations, PR facts,
|
|
866
|
-
// observations), so bounding it would need reducer-partition store
|
|
867
|
-
// machinery out of AUT-545's scope. The reduceBuild re-check stays: it is
|
|
868
|
-
// the legacy-record fallback (a record whose `repo` predates origin
|
|
869
|
-
// normalization is in `mine` via `repoOrigin` but absent from the digest
|
|
870
|
-
// map, which keys on `record.repo`) and it keeps this filter unable to
|
|
871
|
-
// diverge from reduceBuild. Mirrors cli/wait.ts discoverBuilds.
|
|
872
|
-
const digests = await store.getRepoBuildDigests(repo)
|
|
830
|
+
const digests = await readBuildDigests(store, repo)
|
|
873
831
|
for (const record of (await store.listBuilds()).filter(mine)) {
|
|
874
832
|
if (streams.has(record.slug) || retired.has(record.slug)) continue
|
|
875
833
|
const digest = digests.get(record.slug)
|
|
876
834
|
if (digest !== undefined && digest.terminal !== undefined) continue
|
|
877
|
-
const
|
|
878
|
-
const status =
|
|
835
|
+
const current = await readBuildStreamSeed(store, record.slug)
|
|
836
|
+
const status = buildReducer.finish(current.value.build).status
|
|
879
837
|
if (!NONTERMINAL_STATUSES.includes(status)) {
|
|
880
838
|
// Terminal is permanent (reduceBuild never clears it): a legacy
|
|
881
839
|
// record the digest cannot retire is read once, not every pass.
|
|
@@ -886,12 +844,12 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
886
844
|
kind: 'build',
|
|
887
845
|
slug: record.slug,
|
|
888
846
|
named: false,
|
|
889
|
-
|
|
890
|
-
lastSeq:
|
|
847
|
+
seed: current.value,
|
|
848
|
+
lastSeq: current.cursor,
|
|
891
849
|
status,
|
|
892
850
|
}
|
|
893
851
|
streams.set(record.slug, stream)
|
|
894
|
-
baselineMerge(record.slug,
|
|
852
|
+
baselineMerge(record.slug, current.value)
|
|
895
853
|
positions[record.slug] = stream.lastSeq
|
|
896
854
|
}
|
|
897
855
|
}
|
|
@@ -903,18 +861,21 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
903
861
|
positions[REPO_STREAM_KEY] = stream.lastSeq
|
|
904
862
|
try {
|
|
905
863
|
if ((await store.getRepo(repo)) !== null) {
|
|
906
|
-
|
|
907
|
-
const
|
|
908
|
-
const
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
864
|
+
const current = await readRepositoryAttention(store, repo)
|
|
865
|
+
const baseline = resumeSeq ?? current.cursor
|
|
866
|
+
const seed =
|
|
867
|
+
resumeSeq === undefined
|
|
868
|
+
? current.value
|
|
869
|
+
: await historicalRepositoryAttention(store, repo, resumeSeq, current)
|
|
870
|
+
repositoryAttention = new RepositoryAttention(seed)
|
|
912
871
|
attentionInitialized = true
|
|
913
872
|
stream.lastSeq = baseline
|
|
914
873
|
positions[REPO_STREAM_KEY] = baseline
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
874
|
+
if (resumeSeq !== undefined) {
|
|
875
|
+
for (const event of await store.getRepoEvents(repo, baseline)) {
|
|
876
|
+
if (stop) break
|
|
877
|
+
processRepositoryEvent(stream, event)
|
|
878
|
+
}
|
|
918
879
|
}
|
|
919
880
|
if (!stop) checkPersistence()
|
|
920
881
|
} else attentionInitialized = true
|
|
@@ -931,12 +892,11 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
931
892
|
const fresh = await store.getEvents(stream.slug, stream.lastSeq, readOpts)
|
|
932
893
|
for (const event of fresh) {
|
|
933
894
|
if (stop) break
|
|
934
|
-
stream
|
|
935
|
-
processBuildEvent(stream, event, stream.events)
|
|
895
|
+
processBuildEvent(stream, event)
|
|
936
896
|
if (stop) break
|
|
937
897
|
}
|
|
938
898
|
if (fresh.length > 0 && !stop) {
|
|
939
|
-
stream.status =
|
|
899
|
+
stream.status = buildReducer.finish(stream.seed.build).status
|
|
940
900
|
}
|
|
941
901
|
return true
|
|
942
902
|
}
|
|
@@ -951,11 +911,14 @@ export async function abWatch(opts: AbWatchOpts): Promise<void> {
|
|
|
951
911
|
if ((await store.getRepo(repo)) === null) return true
|
|
952
912
|
if (!attentionInitialized) {
|
|
953
913
|
repositoryInitialization ??= (async () => {
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
914
|
+
const current = await readRepositoryAttention(store, repo)
|
|
915
|
+
const resumeSeq = sinceStreams[REPO_STREAM_KEY]
|
|
916
|
+
const baseline = resumeSeq ?? current.cursor
|
|
917
|
+
const seed =
|
|
918
|
+
resumeSeq === undefined
|
|
919
|
+
? current.value
|
|
920
|
+
: await historicalRepositoryAttention(store, repo, resumeSeq, current)
|
|
921
|
+
repositoryAttention = new RepositoryAttention(seed)
|
|
959
922
|
stream.lastSeq = baseline
|
|
960
923
|
positions[REPO_STREAM_KEY] = baseline
|
|
961
924
|
attentionInitialized = true
|
|
@@ -55,7 +55,8 @@ export interface PipelineSource {
|
|
|
55
55
|
export interface PipelineSourceInput {
|
|
56
56
|
slug: string
|
|
57
57
|
record: Pick<BuildRecord, 'branch'>
|
|
58
|
-
events
|
|
58
|
+
events?: readonly AbEvent[]
|
|
59
|
+
recordedBase?: string
|
|
59
60
|
/** 'origin' reads through the forge; 'checkout' reads through git exec in
|
|
60
61
|
* the dispatcher's checkout (build branches are refs of the shared repo). */
|
|
61
62
|
mode: 'origin' | 'checkout'
|
|
@@ -250,7 +251,7 @@ export async function resolvePipelineSource(
|
|
|
250
251
|
}
|
|
251
252
|
}
|
|
252
253
|
}
|
|
253
|
-
const baseSha = recordedBaseSha(events)
|
|
254
|
+
const baseSha = input.recordedBase ?? recordedBaseSha(events ?? [])
|
|
254
255
|
if (baseSha === undefined) {
|
|
255
256
|
onFailure({ kind: 'no-base', detail: 'no recorded base commit (legacy build)' })
|
|
256
257
|
return undefined
|
|
@@ -368,7 +369,7 @@ export async function resolvePipelineSource(
|
|
|
368
369
|
return parsed
|
|
369
370
|
}
|
|
370
371
|
}
|
|
371
|
-
const baseSha = recordedBaseSha(events)
|
|
372
|
+
const baseSha = input.recordedBase ?? recordedBaseSha(events ?? [])
|
|
372
373
|
if (baseSha === undefined) {
|
|
373
374
|
onFailure({ kind: 'no-base', detail: 'no recorded base commit (legacy build)' })
|
|
374
375
|
return undefined
|