@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,620 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine's raw-log index (see `LogIndex`): the projections `reduceBuild`
|
|
3
|
+
* does not carry. Extracted from `engine.ts` as an incremental reducer.
|
|
4
|
+
*
|
|
5
|
+
* The restart boundary (`restartSeq`, the seq of the LAST `spec.revised` in
|
|
6
|
+
* array order) is only known once the whole log has been read, and a later
|
|
7
|
+
* `spec.revised` retroactively demotes events already folded as post-restart.
|
|
8
|
+
* The accumulator therefore has two parts: running state that never depends on
|
|
9
|
+
* the boundary (folded as the loop always did), and an in-array-order candidate
|
|
10
|
+
* list of compact records for every event the post-restart projections read.
|
|
11
|
+
* `finish` replays the candidates against the final `restartSeq`, so any array
|
|
12
|
+
* order — including shuffled — reproduces the whole-array result exactly.
|
|
13
|
+
*/
|
|
14
|
+
import type { AbEvent } from '../events/catalog'
|
|
15
|
+
import { normalizeVerifyCompletion, type EventPayload } from '../events/payloads'
|
|
16
|
+
import {
|
|
17
|
+
verifyPhase,
|
|
18
|
+
type ArtifactRef,
|
|
19
|
+
type Feedback,
|
|
20
|
+
type Finding,
|
|
21
|
+
type ReviewVerdictKind,
|
|
22
|
+
type VerifyOutcome,
|
|
23
|
+
} from '../ontology'
|
|
24
|
+
import { defineReducer } from './incremental'
|
|
25
|
+
|
|
26
|
+
// ── Raw-log index ────────────────────────────────────────────────────────────
|
|
27
|
+
//
|
|
28
|
+
// Every field here is a projection the reducer does not carry (or carries
|
|
29
|
+
// without the seq the engine routes on). Loop and verify progress counts only
|
|
30
|
+
// events with seq > restartSeq (§6.3 immutability: every reviewer approves
|
|
31
|
+
// conformance to ONE spec revision — a drifting spec silently converts
|
|
32
|
+
// approvals into approvals-of-something-else, so approvals of an old spec do
|
|
33
|
+
// not carry across `spec.revised`).
|
|
34
|
+
|
|
35
|
+
export interface VerdictRecord {
|
|
36
|
+
seq: number
|
|
37
|
+
round: number
|
|
38
|
+
verdict: ReviewVerdictKind
|
|
39
|
+
findings: Finding[]
|
|
40
|
+
reason?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface PlanCompletionRecord {
|
|
44
|
+
seq: number
|
|
45
|
+
artifact: ArtifactRef
|
|
46
|
+
verifySteps?: string[]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface RoundRecord {
|
|
50
|
+
startedSeq?: number
|
|
51
|
+
completedSeq?: number
|
|
52
|
+
/** Plan only. Kept separately so approval snapshots this exact completion. */
|
|
53
|
+
planCompletion?: PlanCompletionRecord
|
|
54
|
+
verdict?: VerdictRecord
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface LoopIndex {
|
|
58
|
+
/**
|
|
59
|
+
* Max round in any of this loop's events over the FULL log. Round numbers
|
|
60
|
+
* continue monotonically across spec restarts (next round = max round ever
|
|
61
|
+
* seen + 1) so the log stays unambiguous: "plan round 1" must name exactly
|
|
62
|
+
* one producer run, not one per spec revision.
|
|
63
|
+
*/
|
|
64
|
+
maxRoundEver: number
|
|
65
|
+
/** Max round with post-restart events; 0 when the loop is untouched. */
|
|
66
|
+
maxRound: number
|
|
67
|
+
/** Post-restart per-round event records (latest occurrence wins). */
|
|
68
|
+
rounds: Map<number, RoundRecord>
|
|
69
|
+
/** Latest post-restart revise verdict — the findings-feedback source. */
|
|
70
|
+
latestRevise?: VerdictRecord
|
|
71
|
+
/** seq of the latest post-restart approve verdict (verify cycle boundary). */
|
|
72
|
+
latestApproveSeq: number
|
|
73
|
+
/** Plan only: the completion present when the latest approve verdict landed.
|
|
74
|
+
* A later orphan or superseded completion cannot replace this snapshot. */
|
|
75
|
+
approvedPlan?: PlanCompletionRecord
|
|
76
|
+
/** Findings per round (index round-1), post-restart — stalledChains input.
|
|
77
|
+
* The reducer's reviewFindings spans restarts; this one must not. */
|
|
78
|
+
findingsByRound: Finding[][]
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface VerifyRecord {
|
|
82
|
+
seq: number
|
|
83
|
+
step: string
|
|
84
|
+
attempt: number
|
|
85
|
+
outcome: VerifyOutcome
|
|
86
|
+
report?: ArtifactRef
|
|
87
|
+
reason?: string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface GuidanceDelivery {
|
|
91
|
+
escalation: string
|
|
92
|
+
/** seq of the matching `session.started` that made the carrier actionable. */
|
|
93
|
+
seq: number
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ReconcileStartRecord {
|
|
97
|
+
seq: number
|
|
98
|
+
attempt: number
|
|
99
|
+
baseSha: string
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface ReconcileCompletionRecord extends ReconcileStartRecord {
|
|
103
|
+
completedSeq: number
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface ReconcileProgressRecord {
|
|
107
|
+
seq: number
|
|
108
|
+
conflictSeq: number
|
|
109
|
+
attempt: number
|
|
110
|
+
baseSha: string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface LogIndex {
|
|
114
|
+
/** seq of the latest `spec.revised`, else 0 — the restart boundary (§6.3). */
|
|
115
|
+
restartSeq: number
|
|
116
|
+
plan: LoopIndex
|
|
117
|
+
code: LoopIndex
|
|
118
|
+
/** Post-restart `verify.completed` facts with seq, for the same sequence-based
|
|
119
|
+
* cycle boundary query exposed by the reducer (§15.6-A). */
|
|
120
|
+
verifyCompleted: VerifyRecord[]
|
|
121
|
+
/** Post-restart `verify.started` facts — a crashed step re-runs at the SAME
|
|
122
|
+
* attempt (§15.6-C), so the current cycle's attempt must be readable from
|
|
123
|
+
* its start events even before any completion lands. */
|
|
124
|
+
verifyStarted: { seq: number; attempt: number }[]
|
|
125
|
+
/**
|
|
126
|
+
* Max verify attempt in any `verify.started`/`verify.completed` over the
|
|
127
|
+
* FULL log. Attempt numbers continue monotonically across spec restarts and
|
|
128
|
+
* reconcile cycles — the same rationale as LoopIndex.maxRoundEver: the log
|
|
129
|
+
* stays unambiguous ("verify attempt 2" names exactly one cycle) and D5
|
|
130
|
+
* failure keys (verify:<step>, round = attempt) never collide across cycles.
|
|
131
|
+
* This high-water allocates attempt numbers; current-cycle membership is
|
|
132
|
+
* independently sequence-based.
|
|
133
|
+
*/
|
|
134
|
+
maxVerifyAttemptEver: number
|
|
135
|
+
/** seq of the latest post-restart `reconcile.completed` — cycle boundary
|
|
136
|
+
* input: reconciliation changed code, verify re-runs in full (§15.7). */
|
|
137
|
+
lastReconcileCompletedSeq: number
|
|
138
|
+
/** seq of the latest post-restart `implement.completed` — a verify failure
|
|
139
|
+
* with an implement round after it was already routed (§15.6-A). */
|
|
140
|
+
lastImplementCompletedSeq: number
|
|
141
|
+
/** Post-restart `finalize.completed` seen (the reducer only projects pr). */
|
|
142
|
+
finalizeCompleted: boolean
|
|
143
|
+
/** Post-restart `finalize.step-completed` steps, ok true OR false —
|
|
144
|
+
* post-steps are failure-tolerant (§5), so any completion counts. */
|
|
145
|
+
finalizeStepsDone: Set<string>
|
|
146
|
+
/** Latest `pr.conflicted` (full log — the epilogue is restart-orthogonal);
|
|
147
|
+
* only its seq is needed for policy/dedupe. Its baseSha is detection-time
|
|
148
|
+
* evidence, not the reconcile merge target (§15.7). */
|
|
149
|
+
lastConflict?: { seq: number }
|
|
150
|
+
/** A `reconcile.started` after lastConflict without its completion — a
|
|
151
|
+
* crashed reconcile re-runs the SAME attempt from its start (§15.6-C). */
|
|
152
|
+
conflictReconcileStarted?: { attempt: number }
|
|
153
|
+
/** Most recent completed reconcile, paired with the latest same-occurrence
|
|
154
|
+
* start so crash retries use the base that actually completed. */
|
|
155
|
+
lastReconcileCompleted?: ReconcileCompletionRecord
|
|
156
|
+
/** Explicit authoritative observation for the current repeat conflict. */
|
|
157
|
+
lastConflictProgressCheck?: ReconcileProgressRecord
|
|
158
|
+
/** Distinct completed attempts observed still conflicted at the same base.
|
|
159
|
+
* A later attempt start supplies the observation for pre-event logs. */
|
|
160
|
+
reconcileNoProgressCount: number
|
|
161
|
+
/** Guidance carriers that reached a matching durable session launch.
|
|
162
|
+
* Plan, implement, and agent-verify starts may cite feedback (§15.3), but a
|
|
163
|
+
* citation alone remains recoverable across the pre-launch crash boundary.
|
|
164
|
+
* Only a later `session.started` for the same phase and round/attempt marks
|
|
165
|
+
* that answer delivered (§15.6-B). */
|
|
166
|
+
guidanceDeliveries: GuidanceDelivery[]
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const LOG_INDEX_REDUCER_VERSION = 1
|
|
170
|
+
|
|
171
|
+
type ReviewVerdictPayload = EventPayload<'plan-review.verdict'>
|
|
172
|
+
|
|
173
|
+
type LoopName = 'plan' | 'code'
|
|
174
|
+
|
|
175
|
+
/** A post-restart candidate: the fields the replay reads, in array order. */
|
|
176
|
+
export type LogIndexCandidate =
|
|
177
|
+
| { kind: 'round-start'; loop: LoopName; seq: number; round: number }
|
|
178
|
+
| {
|
|
179
|
+
kind: 'plan-completed'
|
|
180
|
+
seq: number
|
|
181
|
+
round: number
|
|
182
|
+
artifact: ArtifactRef
|
|
183
|
+
verifySteps?: string[]
|
|
184
|
+
}
|
|
185
|
+
| { kind: 'round-touch'; loop: LoopName; seq: number; round: number }
|
|
186
|
+
| { kind: 'round-completed'; loop: LoopName; seq: number; round: number }
|
|
187
|
+
| {
|
|
188
|
+
kind: 'verdict'
|
|
189
|
+
loop: LoopName
|
|
190
|
+
seq: number
|
|
191
|
+
round: number
|
|
192
|
+
verdict: ReviewVerdictKind
|
|
193
|
+
findings: Finding[]
|
|
194
|
+
reason?: string
|
|
195
|
+
}
|
|
196
|
+
| { kind: 'implement-completed'; seq: number }
|
|
197
|
+
| { kind: 'verify-started'; seq: number; attempt: number }
|
|
198
|
+
| { kind: 'verify-completed'; record: VerifyRecord }
|
|
199
|
+
| { kind: 'finalize-completed'; seq: number }
|
|
200
|
+
| { kind: 'finalize-step'; seq: number; step: string }
|
|
201
|
+
| { kind: 'reconcile-completed'; seq: number }
|
|
202
|
+
|
|
203
|
+
export interface LogIndexAcc {
|
|
204
|
+
/** Full-log running state. */
|
|
205
|
+
restartSeq: number
|
|
206
|
+
maxRoundEver: { plan: number; code: number }
|
|
207
|
+
maxVerifyAttemptEver: number
|
|
208
|
+
pendingGuidanceStarts: Record<string, { escalation: string; seq: number }>
|
|
209
|
+
guidanceDeliveries: GuidanceDelivery[]
|
|
210
|
+
lastConflict?: { seq: number }
|
|
211
|
+
conflictSeqs: number[]
|
|
212
|
+
conflictReconcileStarted?: { attempt: number }
|
|
213
|
+
activeReconcileStart?: ReconcileStartRecord
|
|
214
|
+
reconcileStarts: ReconcileStartRecord[]
|
|
215
|
+
reconcileCompletions: ReconcileCompletionRecord[]
|
|
216
|
+
reconcileProgressChecks: ReconcileProgressRecord[]
|
|
217
|
+
/** Post-restart candidates, replayed in `finish` against the final restart. */
|
|
218
|
+
candidates: LogIndexCandidate[]
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function initialLogIndexAcc(): LogIndexAcc {
|
|
222
|
+
return {
|
|
223
|
+
restartSeq: 0,
|
|
224
|
+
maxRoundEver: { plan: 0, code: 0 },
|
|
225
|
+
maxVerifyAttemptEver: 0,
|
|
226
|
+
pendingGuidanceStarts: {},
|
|
227
|
+
guidanceDeliveries: [],
|
|
228
|
+
conflictSeqs: [],
|
|
229
|
+
reconcileStarts: [],
|
|
230
|
+
reconcileCompletions: [],
|
|
231
|
+
reconcileProgressChecks: [],
|
|
232
|
+
candidates: [],
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function emptyLoop(): LoopIndex {
|
|
237
|
+
return {
|
|
238
|
+
maxRoundEver: 0,
|
|
239
|
+
maxRound: 0,
|
|
240
|
+
rounds: new Map(),
|
|
241
|
+
latestApproveSeq: 0,
|
|
242
|
+
findingsByRound: [],
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function foldLogIndex(acc: LogIndexAcc, events: readonly AbEvent[]): void {
|
|
247
|
+
const noteRound = (loop: LoopName, round: number): void => {
|
|
248
|
+
if (round > acc.maxRoundEver[loop]) acc.maxRoundEver[loop] = round
|
|
249
|
+
}
|
|
250
|
+
const noteGuidanceStart = (key: string, feedback: Feedback | undefined, seq: number): void => {
|
|
251
|
+
// A newer start is the authoritative carrier for this exact occurrence.
|
|
252
|
+
// In particular, a guidance-free retry must not let its later session
|
|
253
|
+
// consume an older citation that never launched.
|
|
254
|
+
if (feedback !== undefined && 'guidance' in feedback) {
|
|
255
|
+
acc.pendingGuidanceStarts[key] = {
|
|
256
|
+
escalation: feedback.guidance.escalation,
|
|
257
|
+
seq,
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
delete acc.pendingGuidanceStarts[key]
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
for (const event of events) {
|
|
265
|
+
switch (event.type) {
|
|
266
|
+
case 'spec.revised':
|
|
267
|
+
acc.restartSeq = event.seq
|
|
268
|
+
break
|
|
269
|
+
case 'plan.started':
|
|
270
|
+
noteGuidanceStart(`plan@${event.payload.round}`, event.payload.feedback, event.seq)
|
|
271
|
+
noteRound('plan', event.payload.round)
|
|
272
|
+
acc.candidates.push({
|
|
273
|
+
kind: 'round-start',
|
|
274
|
+
loop: 'plan',
|
|
275
|
+
seq: event.seq,
|
|
276
|
+
round: event.payload.round,
|
|
277
|
+
})
|
|
278
|
+
break
|
|
279
|
+
case 'plan.completed':
|
|
280
|
+
noteRound('plan', event.payload.round)
|
|
281
|
+
acc.candidates.push({
|
|
282
|
+
kind: 'plan-completed',
|
|
283
|
+
seq: event.seq,
|
|
284
|
+
round: event.payload.round,
|
|
285
|
+
artifact: event.payload.artifact,
|
|
286
|
+
...(event.payload.verifySteps !== undefined
|
|
287
|
+
? { verifySteps: [...event.payload.verifySteps] }
|
|
288
|
+
: {}),
|
|
289
|
+
})
|
|
290
|
+
break
|
|
291
|
+
case 'plan-review.started':
|
|
292
|
+
noteRound('plan', event.payload.round)
|
|
293
|
+
acc.candidates.push({
|
|
294
|
+
kind: 'round-touch',
|
|
295
|
+
loop: 'plan',
|
|
296
|
+
seq: event.seq,
|
|
297
|
+
round: event.payload.round,
|
|
298
|
+
})
|
|
299
|
+
break
|
|
300
|
+
case 'plan-review.verdict':
|
|
301
|
+
noteRound('plan', event.payload.round)
|
|
302
|
+
acc.candidates.push(verdictCandidate('plan', event.payload, event.seq))
|
|
303
|
+
break
|
|
304
|
+
|
|
305
|
+
case 'implement.started':
|
|
306
|
+
noteGuidanceStart(`implement@${event.payload.round}`, event.payload.feedback, event.seq)
|
|
307
|
+
noteRound('code', event.payload.round)
|
|
308
|
+
acc.candidates.push({
|
|
309
|
+
kind: 'round-start',
|
|
310
|
+
loop: 'code',
|
|
311
|
+
seq: event.seq,
|
|
312
|
+
round: event.payload.round,
|
|
313
|
+
})
|
|
314
|
+
break
|
|
315
|
+
case 'implement.completed':
|
|
316
|
+
noteRound('code', event.payload.round)
|
|
317
|
+
acc.candidates.push({
|
|
318
|
+
kind: 'round-completed',
|
|
319
|
+
loop: 'code',
|
|
320
|
+
seq: event.seq,
|
|
321
|
+
round: event.payload.round,
|
|
322
|
+
})
|
|
323
|
+
acc.candidates.push({ kind: 'implement-completed', seq: event.seq })
|
|
324
|
+
break
|
|
325
|
+
case 'code-review.started':
|
|
326
|
+
noteRound('code', event.payload.round)
|
|
327
|
+
acc.candidates.push({
|
|
328
|
+
kind: 'round-touch',
|
|
329
|
+
loop: 'code',
|
|
330
|
+
seq: event.seq,
|
|
331
|
+
round: event.payload.round,
|
|
332
|
+
})
|
|
333
|
+
break
|
|
334
|
+
case 'code-review.verdict':
|
|
335
|
+
noteRound('code', event.payload.round)
|
|
336
|
+
acc.candidates.push(verdictCandidate('code', event.payload, event.seq))
|
|
337
|
+
break
|
|
338
|
+
|
|
339
|
+
case 'verify.started':
|
|
340
|
+
noteGuidanceStart(
|
|
341
|
+
`${verifyPhase(event.payload.step)}@${event.payload.attempt}`,
|
|
342
|
+
event.payload.feedback,
|
|
343
|
+
event.seq,
|
|
344
|
+
)
|
|
345
|
+
acc.maxVerifyAttemptEver = Math.max(acc.maxVerifyAttemptEver, event.payload.attempt)
|
|
346
|
+
acc.candidates.push({
|
|
347
|
+
kind: 'verify-started',
|
|
348
|
+
seq: event.seq,
|
|
349
|
+
attempt: event.payload.attempt,
|
|
350
|
+
})
|
|
351
|
+
break
|
|
352
|
+
case 'session.started': {
|
|
353
|
+
if (event.payload.round === undefined) break
|
|
354
|
+
const key = `${event.payload.phase}@${event.payload.round}`
|
|
355
|
+
const carrier = acc.pendingGuidanceStarts[key]
|
|
356
|
+
if (carrier !== undefined && carrier.seq < event.seq) {
|
|
357
|
+
acc.guidanceDeliveries.push({
|
|
358
|
+
escalation: carrier.escalation,
|
|
359
|
+
seq: event.seq,
|
|
360
|
+
})
|
|
361
|
+
delete acc.pendingGuidanceStarts[key]
|
|
362
|
+
}
|
|
363
|
+
break
|
|
364
|
+
}
|
|
365
|
+
case 'verify.completed': {
|
|
366
|
+
const result = normalizeVerifyCompletion(event.payload)
|
|
367
|
+
acc.maxVerifyAttemptEver = Math.max(acc.maxVerifyAttemptEver, result.attempt)
|
|
368
|
+
acc.candidates.push({
|
|
369
|
+
kind: 'verify-completed',
|
|
370
|
+
record: {
|
|
371
|
+
seq: event.seq,
|
|
372
|
+
step: result.step,
|
|
373
|
+
attempt: result.attempt,
|
|
374
|
+
outcome: result.outcome,
|
|
375
|
+
...(result.report !== undefined ? { report: result.report } : {}),
|
|
376
|
+
...(result.reason !== undefined ? { reason: result.reason } : {}),
|
|
377
|
+
},
|
|
378
|
+
})
|
|
379
|
+
break
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
case 'finalize.completed':
|
|
383
|
+
acc.candidates.push({ kind: 'finalize-completed', seq: event.seq })
|
|
384
|
+
break
|
|
385
|
+
case 'finalize.step-completed':
|
|
386
|
+
acc.candidates.push({
|
|
387
|
+
kind: 'finalize-step',
|
|
388
|
+
seq: event.seq,
|
|
389
|
+
step: event.payload.step,
|
|
390
|
+
})
|
|
391
|
+
break
|
|
392
|
+
|
|
393
|
+
case 'pr.conflicted':
|
|
394
|
+
acc.lastConflict = { seq: event.seq }
|
|
395
|
+
acc.conflictSeqs.push(event.seq)
|
|
396
|
+
delete acc.conflictReconcileStarted
|
|
397
|
+
break
|
|
398
|
+
case 'reconcile.progress-checked':
|
|
399
|
+
acc.reconcileProgressChecks.push({ seq: event.seq, ...event.payload })
|
|
400
|
+
break
|
|
401
|
+
case 'reconcile.started': {
|
|
402
|
+
const start = { seq: event.seq, ...event.payload }
|
|
403
|
+
acc.reconcileStarts.push(start)
|
|
404
|
+
acc.activeReconcileStart = start
|
|
405
|
+
if (acc.lastConflict !== undefined && event.seq > acc.lastConflict.seq) {
|
|
406
|
+
acc.conflictReconcileStarted = { attempt: event.payload.attempt }
|
|
407
|
+
}
|
|
408
|
+
break
|
|
409
|
+
}
|
|
410
|
+
case 'reconcile.completed':
|
|
411
|
+
acc.candidates.push({ kind: 'reconcile-completed', seq: event.seq })
|
|
412
|
+
if (acc.activeReconcileStart !== undefined) {
|
|
413
|
+
acc.reconcileCompletions.push({
|
|
414
|
+
...acc.activeReconcileStart,
|
|
415
|
+
completedSeq: event.seq,
|
|
416
|
+
})
|
|
417
|
+
delete acc.activeReconcileStart
|
|
418
|
+
}
|
|
419
|
+
delete acc.conflictReconcileStarted
|
|
420
|
+
break
|
|
421
|
+
|
|
422
|
+
default:
|
|
423
|
+
break
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function verdictCandidate(
|
|
429
|
+
loop: LoopName,
|
|
430
|
+
payload: ReviewVerdictPayload,
|
|
431
|
+
seq: number,
|
|
432
|
+
): LogIndexCandidate {
|
|
433
|
+
return {
|
|
434
|
+
kind: 'verdict',
|
|
435
|
+
loop,
|
|
436
|
+
seq,
|
|
437
|
+
round: payload.round,
|
|
438
|
+
verdict: payload.verdict,
|
|
439
|
+
findings: payload.findings,
|
|
440
|
+
...(payload.reason !== undefined ? { reason: payload.reason } : {}),
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function finishLogIndex(acc: LogIndexAcc): LogIndex {
|
|
445
|
+
const { restartSeq } = acc
|
|
446
|
+
const plan = emptyLoop()
|
|
447
|
+
const code = emptyLoop()
|
|
448
|
+
plan.maxRoundEver = acc.maxRoundEver.plan
|
|
449
|
+
code.maxRoundEver = acc.maxRoundEver.code
|
|
450
|
+
const loops = { plan, code }
|
|
451
|
+
const verifyCompleted: VerifyRecord[] = []
|
|
452
|
+
const verifyStarted: { seq: number; attempt: number }[] = []
|
|
453
|
+
const finalizeStepsDone = new Set<string>()
|
|
454
|
+
let lastReconcileCompletedSeq = 0
|
|
455
|
+
let lastImplementCompletedSeq = 0
|
|
456
|
+
let finalizeCompleted = false
|
|
457
|
+
|
|
458
|
+
const roundRecord = (loop: LoopIndex, r: number): RoundRecord => {
|
|
459
|
+
if (r > loop.maxRound) loop.maxRound = r
|
|
460
|
+
let record = loop.rounds.get(r)
|
|
461
|
+
if (record === undefined) {
|
|
462
|
+
record = {}
|
|
463
|
+
loop.rounds.set(r, record)
|
|
464
|
+
}
|
|
465
|
+
return record
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Replay only what is post-restart under the FINAL boundary, in array order.
|
|
469
|
+
for (const candidate of acc.candidates) {
|
|
470
|
+
const seq = candidate.kind === 'verify-completed' ? candidate.record.seq : candidate.seq
|
|
471
|
+
if (seq <= restartSeq) continue
|
|
472
|
+
switch (candidate.kind) {
|
|
473
|
+
case 'round-start':
|
|
474
|
+
roundRecord(loops[candidate.loop], candidate.round).startedSeq = candidate.seq
|
|
475
|
+
break
|
|
476
|
+
case 'plan-completed': {
|
|
477
|
+
const record = roundRecord(plan, candidate.round)
|
|
478
|
+
record.completedSeq = candidate.seq
|
|
479
|
+
record.planCompletion = {
|
|
480
|
+
seq: candidate.seq,
|
|
481
|
+
artifact: candidate.artifact,
|
|
482
|
+
...(candidate.verifySteps !== undefined
|
|
483
|
+
? { verifySteps: [...candidate.verifySteps] }
|
|
484
|
+
: {}),
|
|
485
|
+
}
|
|
486
|
+
break
|
|
487
|
+
}
|
|
488
|
+
case 'round-touch':
|
|
489
|
+
roundRecord(loops[candidate.loop], candidate.round)
|
|
490
|
+
break
|
|
491
|
+
case 'round-completed':
|
|
492
|
+
roundRecord(loops[candidate.loop], candidate.round).completedSeq = candidate.seq
|
|
493
|
+
break
|
|
494
|
+
case 'verdict': {
|
|
495
|
+
const loop = loops[candidate.loop]
|
|
496
|
+
const record = roundRecord(loop, candidate.round)
|
|
497
|
+
const verdict: VerdictRecord = {
|
|
498
|
+
seq: candidate.seq,
|
|
499
|
+
round: candidate.round,
|
|
500
|
+
verdict: candidate.verdict,
|
|
501
|
+
findings: candidate.findings,
|
|
502
|
+
reason: candidate.reason,
|
|
503
|
+
}
|
|
504
|
+
record.verdict = verdict
|
|
505
|
+
if (candidate.verdict === 'revise') loop.latestRevise = verdict
|
|
506
|
+
if (candidate.verdict === 'approve') {
|
|
507
|
+
loop.latestApproveSeq = candidate.seq
|
|
508
|
+
// Snapshot at verdict time. A plan.completed appended later — even
|
|
509
|
+
// for this round — was never reviewed and therefore has no authority.
|
|
510
|
+
if (loop === plan) loop.approvedPlan = record.planCompletion
|
|
511
|
+
}
|
|
512
|
+
// Findings per round, reducer-style padding (rounds without verdicts
|
|
513
|
+
// stay empty — including every pre-restart round, which is the point).
|
|
514
|
+
while (loop.findingsByRound.length < candidate.round) loop.findingsByRound.push([])
|
|
515
|
+
loop.findingsByRound[candidate.round - 1] = candidate.findings
|
|
516
|
+
break
|
|
517
|
+
}
|
|
518
|
+
case 'implement-completed':
|
|
519
|
+
lastImplementCompletedSeq = candidate.seq
|
|
520
|
+
break
|
|
521
|
+
case 'verify-started':
|
|
522
|
+
verifyStarted.push({ seq: candidate.seq, attempt: candidate.attempt })
|
|
523
|
+
break
|
|
524
|
+
case 'verify-completed':
|
|
525
|
+
verifyCompleted.push({ ...candidate.record })
|
|
526
|
+
break
|
|
527
|
+
case 'finalize-completed':
|
|
528
|
+
finalizeCompleted = true
|
|
529
|
+
break
|
|
530
|
+
case 'finalize-step':
|
|
531
|
+
finalizeStepsDone.add(candidate.step)
|
|
532
|
+
break
|
|
533
|
+
case 'reconcile-completed':
|
|
534
|
+
lastReconcileCompletedSeq = candidate.seq
|
|
535
|
+
break
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const { lastConflict, reconcileStarts, reconcileProgressChecks } = acc
|
|
540
|
+
const conflictSeqs = new Set(acc.conflictSeqs)
|
|
541
|
+
|
|
542
|
+
// One completed occurrence per monotonic attempt. A later duplicate
|
|
543
|
+
// completion is safer to pair with its own latest start than with an older
|
|
544
|
+
// aggregate high-water, and malformed unmatched completions classify nothing.
|
|
545
|
+
const completedByAttempt = new Map<number, ReconcileCompletionRecord>()
|
|
546
|
+
for (const completion of acc.reconcileCompletions) {
|
|
547
|
+
completedByAttempt.set(completion.attempt, completion)
|
|
548
|
+
}
|
|
549
|
+
const completed = [...completedByAttempt.values()].sort(
|
|
550
|
+
(left, right) => left.completedSeq - right.completedSeq,
|
|
551
|
+
)
|
|
552
|
+
|
|
553
|
+
let reconcileNoProgressCount = 0
|
|
554
|
+
for (const completion of completed) {
|
|
555
|
+
const nextStart = reconcileStarts.find(
|
|
556
|
+
(start) => start.seq > completion.completedSeq && start.attempt > completion.attempt,
|
|
557
|
+
)
|
|
558
|
+
const observationLimit = nextStart?.seq ?? Number.POSITIVE_INFINITY
|
|
559
|
+
// Explicit checks win over the historical fallback. Use the latest one
|
|
560
|
+
// that could have routed this next occurrence; duplicate conflict facts do
|
|
561
|
+
// not count one completed reconcile more than once.
|
|
562
|
+
const explicit = reconcileProgressChecks
|
|
563
|
+
.filter(
|
|
564
|
+
(check) =>
|
|
565
|
+
check.attempt === completion.attempt &&
|
|
566
|
+
check.seq > completion.completedSeq &&
|
|
567
|
+
check.seq < observationLimit &&
|
|
568
|
+
check.conflictSeq > completion.completedSeq &&
|
|
569
|
+
check.seq > check.conflictSeq &&
|
|
570
|
+
conflictSeqs.has(check.conflictSeq),
|
|
571
|
+
)
|
|
572
|
+
.at(-1)
|
|
573
|
+
const observedBase = explicit?.baseSha ?? nextStart?.baseSha
|
|
574
|
+
if (observedBase === completion.baseSha) reconcileNoProgressCount += 1
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const lastReconcileCompleted = completed.at(-1)
|
|
578
|
+
const lastConflictProgressCheck =
|
|
579
|
+
lastConflict === undefined || lastReconcileCompleted === undefined
|
|
580
|
+
? undefined
|
|
581
|
+
: reconcileProgressChecks
|
|
582
|
+
.filter(
|
|
583
|
+
(check) =>
|
|
584
|
+
check.conflictSeq === lastConflict.seq &&
|
|
585
|
+
check.attempt === lastReconcileCompleted.attempt &&
|
|
586
|
+
check.seq > lastConflict.seq,
|
|
587
|
+
)
|
|
588
|
+
.at(-1)
|
|
589
|
+
|
|
590
|
+
return {
|
|
591
|
+
restartSeq,
|
|
592
|
+
plan,
|
|
593
|
+
code,
|
|
594
|
+
verifyCompleted,
|
|
595
|
+
verifyStarted,
|
|
596
|
+
maxVerifyAttemptEver: acc.maxVerifyAttemptEver,
|
|
597
|
+
lastReconcileCompletedSeq,
|
|
598
|
+
lastImplementCompletedSeq,
|
|
599
|
+
finalizeCompleted,
|
|
600
|
+
finalizeStepsDone,
|
|
601
|
+
lastConflict,
|
|
602
|
+
conflictReconcileStarted: acc.conflictReconcileStarted,
|
|
603
|
+
lastReconcileCompleted,
|
|
604
|
+
lastConflictProgressCheck,
|
|
605
|
+
reconcileNoProgressCount,
|
|
606
|
+
guidanceDeliveries: [...acc.guidanceDeliveries],
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export const logIndexReducer = defineReducer<LogIndexAcc, AbEvent, LogIndex>({
|
|
611
|
+
version: LOG_INDEX_REDUCER_VERSION,
|
|
612
|
+
initial: initialLogIndexAcc,
|
|
613
|
+
fold: foldLogIndex,
|
|
614
|
+
finish: finishLogIndex,
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
/** The whole-array form: `finish(fold(initial(), events))`. */
|
|
618
|
+
export function indexLog(events: readonly AbEvent[]): LogIndex {
|
|
619
|
+
return logIndexReducer.reduce(events)
|
|
620
|
+
}
|