@defrex/autobuild 0.8.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 +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- 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 +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- 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/phases.ts +16 -2
- 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 +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- 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/{remote-index.ts → hosted-index.ts} +3 -2
- 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 +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- 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 +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The repository auto-merge default as a bulk action over in-flight builds.
|
|
3
|
+
*
|
|
4
|
+
* The durable `dispatcher.auto-merge-default-set` fact has always been the
|
|
5
|
+
* claim-time seed; it now also fans out onto every current non-terminal build
|
|
6
|
+
* of the repository, attributed to the operator who toggled it. The fan-out is
|
|
7
|
+
* a dispatcher-tick reconciliation driven by the durable fact — never a
|
|
8
|
+
* write-time walk — so a build claimed concurrently with the toggle and a
|
|
9
|
+
* build observed by a second dispatcher polling the same repository converge
|
|
10
|
+
* on the same state.
|
|
11
|
+
*
|
|
12
|
+
* Idempotency vs. per-build overrides: each answering command records the
|
|
13
|
+
* repository `seq` of the default fact that caused it (`defaultSeq`). A tick
|
|
14
|
+
* applies the default to a build only when the newest fact's seq is strictly
|
|
15
|
+
* newer than the build's recorded `defaultSeq`. When the build's requested
|
|
16
|
+
* state already matches the fact, the tick records a provenance-only
|
|
17
|
+
* `build.auto-merge-default-observed` marker instead of appending a duplicate
|
|
18
|
+
* command, so a later per-build toggle is not mistaken for staleness and
|
|
19
|
+
* reverted. A global toggle therefore re-applies over a per-build choice, a
|
|
20
|
+
* per-build toggle in between is stable, and pressing the toggle twice appends
|
|
21
|
+
* no duplicate request.
|
|
22
|
+
*
|
|
23
|
+
* Eligibility is expressed against `BuildState`, not `effectiveStatus`
|
|
24
|
+
* (dashboard/model.ts), whose contract is "DISPLAY-ONLY — nothing consults
|
|
25
|
+
* this"; a durable write path must not become its first consumer — the same
|
|
26
|
+
* precedent `bulk-control.ts` set for the pause/resume walk.
|
|
27
|
+
*/
|
|
28
|
+
import type { RepositoryEvent } from '../events/repository'
|
|
29
|
+
import { defineReducer } from './incremental'
|
|
30
|
+
import { discardInFlight, type BuildState } from './reducer'
|
|
31
|
+
|
|
32
|
+
/** One `dispatcher.auto-merge-default-set` fact, resolved. `actor` is the
|
|
33
|
+
* event's actor — the human who toggled, `via` marker included — and is the
|
|
34
|
+
* attribution every fan-out command carries. */
|
|
35
|
+
export interface AutoMergeDefaultFact {
|
|
36
|
+
enabled: boolean
|
|
37
|
+
/** The repository event's seq — the fan-out's comparison marker. */
|
|
38
|
+
seq: number
|
|
39
|
+
actor: RepositoryEvent['actor']
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The newest `dispatcher.auto-merge-default-set` fact in the repository
|
|
43
|
+
* journal, or undefined when no default was ever set (the scan runs from the
|
|
44
|
+
* tail because seqs are monotonic, so the first hit is the newest). With
|
|
45
|
+
* `enabled`, the newest fact *matching that value* — the fact a claim-time
|
|
46
|
+
* seed built on a stale pre-tick sample must cite, so a newer opposite fact
|
|
47
|
+
* stays strictly newer than the seed's provenance and the next tick's
|
|
48
|
+
* fan-out can reconcile it (f_b851c0e8). */
|
|
49
|
+
export function latestAutoMergeDefault(
|
|
50
|
+
events: RepositoryEvent[],
|
|
51
|
+
enabled?: boolean,
|
|
52
|
+
): AutoMergeDefaultFact | undefined {
|
|
53
|
+
return pickAutoMergeDefault(autoMergeDefaultReducer.reduce(events), enabled)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const AUTO_MERGE_DEFAULT_REDUCER_VERSION = 1
|
|
57
|
+
|
|
58
|
+
/** The newest `dispatcher.auto-merge-default-set` fact in array order, overall
|
|
59
|
+
* and per value — `latestAutoMergeDefault(events, enabled)` is a lookup in
|
|
60
|
+
* this state, exact for any array order. */
|
|
61
|
+
export interface AutoMergeDefaultFacts {
|
|
62
|
+
latest?: AutoMergeDefaultFact
|
|
63
|
+
latestEnabled?: AutoMergeDefaultFact
|
|
64
|
+
latestDisabled?: AutoMergeDefaultFact
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const autoMergeDefaultReducer = defineReducer<
|
|
68
|
+
AutoMergeDefaultFacts,
|
|
69
|
+
RepositoryEvent,
|
|
70
|
+
AutoMergeDefaultFacts
|
|
71
|
+
>({
|
|
72
|
+
version: AUTO_MERGE_DEFAULT_REDUCER_VERSION,
|
|
73
|
+
initial: () => ({}),
|
|
74
|
+
fold(acc, events) {
|
|
75
|
+
for (const event of events) {
|
|
76
|
+
if (event.type !== 'dispatcher.auto-merge-default-set') continue
|
|
77
|
+
const fact: AutoMergeDefaultFact = {
|
|
78
|
+
enabled: event.payload.enabled,
|
|
79
|
+
seq: event.seq,
|
|
80
|
+
actor: event.actor,
|
|
81
|
+
}
|
|
82
|
+
acc.latest = fact
|
|
83
|
+
if (fact.enabled) acc.latestEnabled = fact
|
|
84
|
+
else acc.latestDisabled = fact
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
finish: (acc) => ({ ...acc }),
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
export function pickAutoMergeDefault(
|
|
91
|
+
facts: AutoMergeDefaultFacts,
|
|
92
|
+
enabled?: boolean,
|
|
93
|
+
): AutoMergeDefaultFact | undefined {
|
|
94
|
+
if (enabled === undefined) return facts.latest
|
|
95
|
+
return enabled ? facts.latestEnabled : facts.latestDisabled
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* A build the default may still act on: not terminal, and not on its way out.
|
|
100
|
+
* A pending abort (an explicit request or an accepted escalation answer) must
|
|
101
|
+
* never receive consent — a merge landing mid-abort is exactly the race this
|
|
102
|
+
* clause exists to close. The same holds for an outstanding discard request:
|
|
103
|
+
* `discardRequest` is present exactly while the build is non-terminal and its
|
|
104
|
+
* discard is unsettled, and the reducer settles it only by terminal completion
|
|
105
|
+
* — consent recorded inside that window can merge a build the operator already
|
|
106
|
+
* asked to discard (in the raced-runner-attachment case the discard is inert
|
|
107
|
+
* for a running build, and the recorded consent later drives the merge in
|
|
108
|
+
* `checkPr`). Direction-blind like the abort clause: the build terminalizes
|
|
109
|
+
* imminently either way. If discard ever grows a mid-flight settlement event,
|
|
110
|
+
* this predicate must be revisited.
|
|
111
|
+
*/
|
|
112
|
+
export function autoMergeDefaultEligible(state: BuildState): boolean {
|
|
113
|
+
if (state.status === 'done' || state.status === 'aborted') return false
|
|
114
|
+
// An empty log is a record whose `build.created` has not landed yet (the
|
|
115
|
+
// crash or one-await window between `createBuild` and the first append):
|
|
116
|
+
// writing an auto-merge command ahead of the immutable facts would leave the
|
|
117
|
+
// log unrecoverable — dispatch recovery rejects a log that does not start
|
|
118
|
+
// with `build.created` (f_647d40be). `lastSeq === 0` is exactly that log.
|
|
119
|
+
if (state.lastSeq === 0) return false
|
|
120
|
+
// An outstanding discard request is the same "on its way out" shape as a
|
|
121
|
+
// pending abort: it settles only by terminal completion (reducer.ts), so
|
|
122
|
+
// consent recorded inside that window can merge a build the operator
|
|
123
|
+
// already asked to discard. Direction-blind, like the abort clause — an
|
|
124
|
+
// OFF fan-out skipping it withdraws nothing that survives the settlement.
|
|
125
|
+
// Shared predicate: the per-build consent guard (build-control.ts) applies
|
|
126
|
+
// the same exclusion to consent-recording writes. A per-build OFF withdrawal
|
|
127
|
+
// stays available there — revocation only shrinks the merge set — which is a
|
|
128
|
+
// stated, directional divergence, not a silent one.
|
|
129
|
+
if (discardInFlight(state)) return false
|
|
130
|
+
return !state.pendingCommands.some((command) => command.command === 'abort')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** What one default fact asks of one build: a real command, a pure provenance
|
|
134
|
+
* advance, or `undefined` for "nothing". */
|
|
135
|
+
export type AutoMergeDefaultTarget = 'request' | 'cancel' | 'observed'
|
|
136
|
+
|
|
137
|
+
/** What one default fact asks of one build, or undefined for "nothing".
|
|
138
|
+
*
|
|
139
|
+
* Seq-first: a build whose recorded provenance is already this fact (or newer)
|
|
140
|
+
* has had its day in court — a per-build toggle made after the fan-out stands
|
|
141
|
+
* until the default moves again. A build that sampled the default at claim time
|
|
142
|
+
* carries the fact's seq too, so a fresh claim is not re-fanned on the next
|
|
143
|
+
* tick.
|
|
144
|
+
*
|
|
145
|
+
* When the requested state already matches the fact, the fact still has to be
|
|
146
|
+
* *recorded* as observed (`'observed'`) so the next tick does not mistake a
|
|
147
|
+
* later per-build command for staleness: the marker advances `defaultSeq`
|
|
148
|
+
* without a duplicate request/cancel (f_28b3fba1). */
|
|
149
|
+
export function autoMergeDefaultTarget(
|
|
150
|
+
state: BuildState,
|
|
151
|
+
fact: AutoMergeDefaultFact,
|
|
152
|
+
): AutoMergeDefaultTarget | undefined {
|
|
153
|
+
if (!autoMergeDefaultEligible(state)) return undefined
|
|
154
|
+
if ((state.autoMerge.defaultSeq ?? 0) >= fact.seq) return undefined
|
|
155
|
+
if (state.autoMerge.requested === fact.enabled) return 'observed'
|
|
156
|
+
return fact.enabled ? 'request' : 'cancel'
|
|
157
|
+
}
|
|
@@ -2,6 +2,7 @@ import type { AbEvent, EventEnvelope, EventWrite } from '../events/catalog'
|
|
|
2
2
|
import { KERNEL } from '../events/envelope'
|
|
3
3
|
import { autoMergeDeferralClasses, type AutoMergeDeferralReason } from '../ports/types'
|
|
4
4
|
import type { BuildStore } from '../store/types'
|
|
5
|
+
import { defineReducer } from './incremental'
|
|
5
6
|
import type { BuildState } from './reducer'
|
|
6
7
|
|
|
7
8
|
/** The complete GitHub `mergeStateStatus` enum. Keeping this list closed is a
|
|
@@ -133,36 +134,104 @@ export function currentDeferralObservation(
|
|
|
133
134
|
events: AbEvent[],
|
|
134
135
|
prNumber: number,
|
|
135
136
|
commandSeq: number,
|
|
136
|
-
):
|
|
137
|
+
): DeferralObservation | undefined {
|
|
138
|
+
return deferralObservationFrom(
|
|
139
|
+
currentDeferralObservationReducer.reduce(events),
|
|
140
|
+
prNumber,
|
|
141
|
+
commandSeq,
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type DeferralObservation = Extract<AbEvent, { type: 'observation.recorded' }>
|
|
146
|
+
|
|
147
|
+
const DEFERRAL_REF_PREFIX = 'auto-merge-gate:pr:'
|
|
148
|
+
|
|
149
|
+
/** One array-order ledger entry. Only the facts the currency predicate reads
|
|
150
|
+
* are kept, with every maximal run of other events collapsed to a barrier
|
|
151
|
+
* carrying the run's max seq: the head scan stops at the first event (of ANY
|
|
152
|
+
* type) with `seq >= observation.seq`, so an unrelated event must still be able
|
|
153
|
+
* to end it. */
|
|
154
|
+
type DeferralLedgerEntry =
|
|
155
|
+
| { t: 'barrier'; seq: number }
|
|
156
|
+
| { t: 'observation'; event: DeferralObservation }
|
|
157
|
+
| { t: 'finalized'; seq: number; headSha: string }
|
|
158
|
+
| { t: 'terminal'; seq: number }
|
|
159
|
+
| { t: 'reconciled'; seq: number }
|
|
160
|
+
|
|
161
|
+
export const CURRENT_DEFERRAL_OBSERVATION_REDUCER_VERSION = 1
|
|
162
|
+
|
|
163
|
+
export interface DeferralLedger {
|
|
164
|
+
entries: DeferralLedgerEntry[]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export const currentDeferralObservationReducer = defineReducer<
|
|
168
|
+
DeferralLedger,
|
|
169
|
+
AbEvent,
|
|
170
|
+
DeferralLedger
|
|
171
|
+
>({
|
|
172
|
+
version: CURRENT_DEFERRAL_OBSERVATION_REDUCER_VERSION,
|
|
173
|
+
initial: () => ({ entries: [] }),
|
|
174
|
+
fold(acc, events) {
|
|
175
|
+
for (const event of events) {
|
|
176
|
+
let entry: DeferralLedgerEntry | undefined
|
|
177
|
+
if (
|
|
178
|
+
event.type === 'observation.recorded' &&
|
|
179
|
+
event.payload.refs?.some((ref) => ref.startsWith(DEFERRAL_REF_PREFIX)) === true
|
|
180
|
+
) {
|
|
181
|
+
entry = { t: 'observation', event }
|
|
182
|
+
} else if (event.type === 'finalize.completed') {
|
|
183
|
+
entry = { t: 'finalized', seq: event.seq, headSha: event.payload.pr.headSha }
|
|
184
|
+
} else if (event.type === 'pr.merged' || event.type === 'pr.closed') {
|
|
185
|
+
entry = { t: 'terminal', seq: event.seq }
|
|
186
|
+
} else if (event.type === 'reconcile.completed') {
|
|
187
|
+
entry = { t: 'reconciled', seq: event.seq }
|
|
188
|
+
}
|
|
189
|
+
if (entry !== undefined) {
|
|
190
|
+
acc.entries.push(entry)
|
|
191
|
+
continue
|
|
192
|
+
}
|
|
193
|
+
const last = acc.entries.at(-1)
|
|
194
|
+
if (last?.t === 'barrier') last.seq = Math.max(last.seq, event.seq)
|
|
195
|
+
else acc.entries.push({ t: 'barrier', seq: event.seq })
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
finish: (acc) => ({ entries: acc.entries.slice() }),
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
function deferralEntrySeq(entry: DeferralLedgerEntry): number {
|
|
202
|
+
return entry.t === 'observation' ? entry.event.seq : entry.seq
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** The current deferral observation for a PR/command over a reduced ledger. */
|
|
206
|
+
export function deferralObservationFrom(
|
|
207
|
+
ledger: DeferralLedger,
|
|
208
|
+
prNumber: number,
|
|
209
|
+
commandSeq: number,
|
|
210
|
+
): DeferralObservation | undefined {
|
|
137
211
|
const marker = autoMergeDeferralRef(prNumber, commandSeq)
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
event.payload.refs?.includes(marker) === true
|
|
145
|
-
) {
|
|
146
|
-
observation = event
|
|
212
|
+
const { entries } = ledger
|
|
213
|
+
let observation: DeferralObservation | undefined
|
|
214
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
215
|
+
const entry = entries[index]
|
|
216
|
+
if (entry?.t === 'observation' && entry.event.payload.refs?.includes(marker) === true) {
|
|
217
|
+
observation = entry.event
|
|
147
218
|
break
|
|
148
219
|
}
|
|
149
220
|
}
|
|
150
221
|
if (observation === undefined) return undefined
|
|
151
222
|
|
|
152
223
|
let headAtObservation: string | undefined
|
|
153
|
-
for (const
|
|
154
|
-
if (
|
|
155
|
-
if (
|
|
224
|
+
for (const entry of entries) {
|
|
225
|
+
if (deferralEntrySeq(entry) >= observation.seq) break
|
|
226
|
+
if (entry.t === 'finalized') headAtObservation = entry.headSha
|
|
156
227
|
}
|
|
157
228
|
|
|
158
|
-
for (const
|
|
159
|
-
if (
|
|
160
|
-
if (
|
|
161
|
-
if (
|
|
162
|
-
if (
|
|
163
|
-
|
|
164
|
-
else if (event.payload.pr.headSha !== headAtObservation) return undefined
|
|
165
|
-
}
|
|
229
|
+
for (const entry of entries) {
|
|
230
|
+
if (entry.t === 'observation' || entry.t === 'barrier') continue
|
|
231
|
+
if (entry.seq <= observation.seq) continue
|
|
232
|
+
if (entry.t === 'terminal' || entry.t === 'reconciled') return undefined
|
|
233
|
+
if (headAtObservation === undefined) headAtObservation = entry.headSha
|
|
234
|
+
else if (entry.headSha !== headAtObservation) return undefined
|
|
166
235
|
}
|
|
167
236
|
return observation
|
|
168
237
|
}
|
|
@@ -186,10 +255,18 @@ export function hasAutoMergeDeferralObservation(
|
|
|
186
255
|
export function currentAutoMergeDeferral(
|
|
187
256
|
events: AbEvent[],
|
|
188
257
|
state: Pick<BuildState, 'autoMerge' | 'pr'>,
|
|
258
|
+
): string | undefined {
|
|
259
|
+
return currentAutoMergeDeferralFrom(currentDeferralObservationReducer.reduce(events), state)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** `currentAutoMergeDeferral` over an already-reduced ledger. */
|
|
263
|
+
export function currentAutoMergeDeferralFrom(
|
|
264
|
+
ledger: DeferralLedger,
|
|
265
|
+
state: Pick<BuildState, 'autoMerge' | 'pr'>,
|
|
189
266
|
): string | undefined {
|
|
190
267
|
const pending = pendingAutoMerge(state)
|
|
191
268
|
if (pending?.enabled !== true || state.pr === undefined) return undefined
|
|
192
|
-
return
|
|
269
|
+
return deferralObservationFrom(ledger, state.pr.number, pending.commandSeq)?.payload.summary
|
|
193
270
|
}
|
|
194
271
|
|
|
195
272
|
const DEFERRAL_SUMMARIES = {
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* is authoritative; fresh repositories retain the historical process defaults.
|
|
5
5
|
*/
|
|
6
6
|
import type { RepositoryEvent } from '../events/repository'
|
|
7
|
+
import { defineReducer, type IncrementalReducer } from './incremental'
|
|
7
8
|
|
|
8
9
|
export const DEFAULT_DISPATCH_INTAKE = true
|
|
9
10
|
export const DEFAULT_DISPATCH_PAUSED = false
|
|
@@ -21,32 +22,36 @@ export interface DispatchSettings {
|
|
|
21
22
|
defaultAutoMerge: boolean
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
/** The carried accumulator: the settings plus the seq of the fact that set
|
|
26
|
+
* each, which decides last-writer-wins independent of array order. */
|
|
27
|
+
export interface DispatchSettingsAcc extends DispatchSettings {
|
|
28
|
+
intakeSeq: number
|
|
29
|
+
pausedSeq: number
|
|
30
|
+
autoMergeSeq: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Bump when `DispatchSettingsAcc`'s shape or fold semantics change. */
|
|
34
|
+
export const DISPATCH_SETTINGS_REDUCER_VERSION = 1
|
|
31
35
|
|
|
36
|
+
function foldDispatchSettings(acc: DispatchSettingsAcc, events: readonly RepositoryEvent[]): void {
|
|
32
37
|
for (const event of events) {
|
|
33
38
|
switch (event.type) {
|
|
34
39
|
case 'dispatcher.intake-set':
|
|
35
|
-
if (event.seq > intakeSeq) {
|
|
36
|
-
intake = event.payload.enabled
|
|
37
|
-
intakeSeq = event.seq
|
|
40
|
+
if (event.seq > acc.intakeSeq) {
|
|
41
|
+
acc.intake = event.payload.enabled
|
|
42
|
+
acc.intakeSeq = event.seq
|
|
38
43
|
}
|
|
39
44
|
break
|
|
40
45
|
case 'dispatcher.pause-set':
|
|
41
|
-
if (event.seq > pausedSeq) {
|
|
42
|
-
paused = event.payload.enabled
|
|
43
|
-
pausedSeq = event.seq
|
|
46
|
+
if (event.seq > acc.pausedSeq) {
|
|
47
|
+
acc.paused = event.payload.enabled
|
|
48
|
+
acc.pausedSeq = event.seq
|
|
44
49
|
}
|
|
45
50
|
break
|
|
46
51
|
case 'dispatcher.auto-merge-default-set':
|
|
47
|
-
if (event.seq > autoMergeSeq) {
|
|
48
|
-
defaultAutoMerge = event.payload.enabled
|
|
49
|
-
autoMergeSeq = event.seq
|
|
52
|
+
if (event.seq > acc.autoMergeSeq) {
|
|
53
|
+
acc.defaultAutoMerge = event.payload.enabled
|
|
54
|
+
acc.autoMergeSeq = event.seq
|
|
50
55
|
}
|
|
51
56
|
break
|
|
52
57
|
default:
|
|
@@ -55,6 +60,26 @@ export function reduceDispatchSettings(events: RepositoryEvent[]): DispatchSetti
|
|
|
55
60
|
break
|
|
56
61
|
}
|
|
57
62
|
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const dispatchSettingsReducer: IncrementalReducer<
|
|
66
|
+
DispatchSettingsAcc,
|
|
67
|
+
RepositoryEvent,
|
|
68
|
+
DispatchSettings
|
|
69
|
+
> = defineReducer({
|
|
70
|
+
version: DISPATCH_SETTINGS_REDUCER_VERSION,
|
|
71
|
+
initial: () => ({
|
|
72
|
+
intake: DEFAULT_DISPATCH_INTAKE,
|
|
73
|
+
paused: DEFAULT_DISPATCH_PAUSED,
|
|
74
|
+
defaultAutoMerge: DEFAULT_DISPATCH_AUTO_MERGE,
|
|
75
|
+
intakeSeq: 0,
|
|
76
|
+
pausedSeq: 0,
|
|
77
|
+
autoMergeSeq: 0,
|
|
78
|
+
}),
|
|
79
|
+
fold: foldDispatchSettings,
|
|
80
|
+
finish: ({ intake, paused, defaultAutoMerge }) => ({ intake, paused, defaultAutoMerge }),
|
|
81
|
+
})
|
|
58
82
|
|
|
59
|
-
|
|
83
|
+
export function reduceDispatchSettings(events: RepositoryEvent[]): DispatchSettings {
|
|
84
|
+
return dispatchSettingsReducer.reduce(events)
|
|
60
85
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { RepositoryEvent } from '../events/repository'
|
|
2
2
|
import type { ArtifactRef } from '../ontology'
|
|
3
|
+
import type { IncrementalReducer } from './incremental'
|
|
3
4
|
|
|
4
5
|
export type DispatchHealth = 'starting' | 'running' | 'stopped' | 'failed'
|
|
5
6
|
|
|
@@ -139,3 +140,25 @@ export function reduceDispatchStatus(
|
|
|
139
140
|
}
|
|
140
141
|
return state
|
|
141
142
|
}
|
|
143
|
+
|
|
144
|
+
/** Bump when `DispatchStatus`'s shape or fold semantics change (it is its own
|
|
145
|
+
* accumulator: `lastSeq` makes the fold skip already-seen events). */
|
|
146
|
+
export const DISPATCH_STATUS_REDUCER_VERSION = 1
|
|
147
|
+
|
|
148
|
+
/** The incremental form of `reduceDispatchStatus` for one run. The run is a
|
|
149
|
+
* parameter, so this is a factory. */
|
|
150
|
+
export function dispatchStatusReducer(
|
|
151
|
+
run: string,
|
|
152
|
+
): IncrementalReducer<DispatchStatus, RepositoryEvent, DispatchStatus> {
|
|
153
|
+
return {
|
|
154
|
+
version: DISPATCH_STATUS_REDUCER_VERSION,
|
|
155
|
+
initial: () => ({ run, health: 'starting', roleWarnings: [], diagnostics: [], lastSeq: 0 }),
|
|
156
|
+
advance: (acc, events) => reduceDispatchStatus(events, run, acc),
|
|
157
|
+
finish: (acc) => ({
|
|
158
|
+
...acc,
|
|
159
|
+
roleWarnings: [...acc.roleWarnings],
|
|
160
|
+
diagnostics: [...acc.diagnostics],
|
|
161
|
+
}),
|
|
162
|
+
reduce: (events) => reduceDispatchStatus(events, run),
|
|
163
|
+
}
|
|
164
|
+
}
|