@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.
Files changed (94) hide show
  1. package/README.md +1 -1
  2. package/SPEC.md +19 -1
  3. package/bin/ab-dispatch-kernel.ts +4 -6
  4. package/docs/architecture.md +96 -70
  5. package/docs/hosted-service.md +2 -2
  6. package/docs/operator-api.md +1 -1
  7. package/docs/remote-store-protocol.md +84 -2
  8. package/package.json +4 -3
  9. package/packages/core/src/cli/attention.ts +18 -164
  10. package/packages/core/src/cli/build-control.ts +11 -15
  11. package/packages/core/src/cli/bulk-control.ts +3 -2
  12. package/packages/core/src/cli/context.ts +16 -78
  13. package/packages/core/src/cli/dashboard/model.ts +36 -22
  14. package/packages/core/src/cli/dispatch-frontend.ts +24 -67
  15. package/packages/core/src/cli/dispatch-process.ts +14 -7
  16. package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
  17. package/packages/core/src/cli/dispatch.ts +148 -79
  18. package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
  19. package/packages/core/src/cli/harvest.ts +71 -39
  20. package/packages/core/src/cli/historical-build-state.ts +27 -0
  21. package/packages/core/src/cli/main.ts +3 -3
  22. package/packages/core/src/cli/pr-attachments.ts +10 -13
  23. package/packages/core/src/cli/pr-summary-publication.ts +27 -60
  24. package/packages/core/src/cli/pr-summary.ts +43 -13
  25. package/packages/core/src/cli/publication-settlement.ts +10 -20
  26. package/packages/core/src/cli/repository-status.ts +38 -2
  27. package/packages/core/src/cli/status.ts +62 -32
  28. package/packages/core/src/cli/terminals.ts +25 -142
  29. package/packages/core/src/cli/ticket.ts +2 -2
  30. package/packages/core/src/cli/wait.ts +91 -84
  31. package/packages/core/src/cli/watch.ts +60 -97
  32. package/packages/core/src/config/pipeline-source.ts +4 -3
  33. package/packages/core/src/events/repository.ts +7 -3
  34. package/packages/core/src/integration/harness.ts +1 -2
  35. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2429 -9
  36. package/packages/core/src/kernel/auto-merge.ts +19 -3
  37. package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
  38. package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
  39. package/packages/core/src/kernel/harvest.ts +25 -23
  40. package/packages/core/src/kernel/log-index-value.ts +38 -0
  41. package/packages/core/src/kernel/read-facts.ts +637 -0
  42. package/packages/core/src/kernel/reducer-registry.ts +137 -3
  43. package/packages/core/src/kernel/repository-attention.ts +179 -0
  44. package/packages/core/src/kernel/service-facts.ts +135 -0
  45. package/packages/core/src/kernel/terminal-guard.ts +133 -0
  46. package/packages/core/src/operator/control.ts +5 -6
  47. package/packages/core/src/operator/index.ts +2 -0
  48. package/packages/core/src/operator/query.ts +37 -20
  49. package/packages/core/src/operator/read-view.ts +4 -13
  50. package/packages/core/src/operator/requests.ts +2 -14
  51. package/packages/core/src/operator/sandbox.ts +10 -9
  52. package/packages/core/src/operator/tickets.ts +2 -2
  53. package/packages/core/src/plugin-sdk/index.ts +13 -0
  54. package/packages/core/src/processes/build-child.ts +6 -6
  55. package/packages/core/src/processes/build-runner.ts +62 -70
  56. package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
  57. package/packages/core/src/processes/dispatcher.ts +113 -173
  58. package/packages/core/src/processes/harvest-execution-state.ts +15 -9
  59. package/packages/core/src/processes/harvest-runner.ts +34 -70
  60. package/packages/core/src/processes/harvest.ts +38 -15
  61. package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
  62. package/packages/core/src/processes/orchestrator-tick.ts +2 -2
  63. package/packages/core/src/processes/publication-loss.ts +35 -21
  64. package/packages/core/src/processes/repo-view.ts +229 -1134
  65. package/packages/core/src/processes/tick-publication.ts +110 -0
  66. package/packages/core/src/store/adapter.ts +23 -0
  67. package/packages/core/src/store/build-scope.ts +17 -0
  68. package/packages/core/src/store/contract.ts +251 -0
  69. package/packages/core/src/store/local/schema.ts +1 -0
  70. package/packages/core/src/store/local/store.ts +179 -54
  71. package/packages/core/src/store/memory.ts +119 -29
  72. package/packages/core/src/store/phase-session.ts +17 -0
  73. package/packages/core/src/store/reduced-reads.ts +239 -0
  74. package/packages/core/src/store/reducer-value-shapes.json +14056 -0
  75. package/packages/core/src/store/reducer-value-validation.ts +96 -0
  76. package/packages/core/src/store/reducer-values.ts +145 -0
  77. package/packages/core/src/store/remote/client.ts +41 -0
  78. package/packages/core/src/store/remote/compatibility.ts +2 -0
  79. package/packages/core/src/store/remote/index.ts +19 -0
  80. package/packages/core/src/store/remote/protocol.ts +16 -0
  81. package/packages/core/src/store/remote/version.ts +3 -1
  82. package/packages/core/src/store/repo-state-events.ts +1 -1
  83. package/packages/core/src/store/repo-tick.ts +72 -0
  84. package/packages/core/src/store/session-handle.ts +19 -0
  85. package/packages/core/src/store/snapshots.ts +10 -0
  86. package/packages/core/src/store/ticket-assets.ts +4 -4
  87. package/packages/core/src/store/types.ts +39 -2
  88. package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
  89. package/packages/core/src/testing/index.ts +8 -0
  90. package/packages/core/src/testing/reduced-read-budget.ts +462 -0
  91. package/skills/guide/SKILL.md +6 -1
  92. package/skills/guide/references/operator-api.md +1 -1
  93. package/skills/guide/references/remote-store-protocol.md +84 -2
  94. package/templates/hosted-service/package.json +2 -2
@@ -0,0 +1,637 @@
1
+ import { encodeLog } from './log-index-value'
2
+ import { summaryBoundaryReducer, summaryRequestsReducer } from './pr-summary'
3
+ import { infrastructureFailureResetReducer } from '../processes/infrastructure-failure-budget'
4
+ import {
5
+ frozenPrImageHostReducer,
6
+ currentPrAttachmentsReducer,
7
+ hostedPrAttachmentsReducer,
8
+ pendingPrAttachmentReclaimsReducer,
9
+ } from './pr-attachments'
10
+ import { isRemoteWorkspace } from '../events/workspace-remote'
11
+ /** JSON-safe projections for persisted current-state consumers. */
12
+ import type { AbEvent } from '../events/catalog'
13
+ import { defineReducer } from './incremental'
14
+ import { buildReducer, type BuildAcc } from './reducer'
15
+ import {
16
+ currentDeferralObservationReducer,
17
+ mergeAttentionFromFacts,
18
+ type DeferralLedger,
19
+ } from './auto-merge'
20
+ import { logIndexReducer } from './log-index'
21
+ import { publicationStateReducer } from '../processes/publication-state'
22
+ import { executionNoProgressReducer } from '../processes/execution-no-progress'
23
+ import {
24
+ openExecutionReducer,
25
+ lastExecutionOutcomeReducer,
26
+ } from '../processes/execution-settlement'
27
+ import {
28
+ baseBranchReducer,
29
+ provisionMarkerReducer,
30
+ recoveryCheckpointReducer,
31
+ openBuildWorkspaceReducer,
32
+ } from '../processes/dispatcher-selectors'
33
+ import { dashboardFactsReducer } from '../cli/dashboard/facts'
34
+
35
+ export interface BuildStreamSeed {
36
+ build: BuildAcc
37
+ deferral: DeferralLedger
38
+ mergeAttentionPrs: number[]
39
+ /** Terminal identity can precede later observations; retain it at the seed cursor. */
40
+ terminalEvent?: AbEvent
41
+ }
42
+ // Bump with either constituent's semantics or historical eligibility semantics.
43
+ export const buildStreamSeedReducer = defineReducer<BuildStreamSeed, AbEvent, BuildStreamSeed>({
44
+ version: 1 + buildReducer.version + currentDeferralObservationReducer.version,
45
+ initial: () => ({
46
+ build: buildReducer.initial(),
47
+ deferral: currentDeferralObservationReducer.initial(),
48
+ mergeAttentionPrs: [],
49
+ }),
50
+ fold(acc, events) {
51
+ for (const event of events) {
52
+ if (event.type === 'build.completed' || event.type === 'build.aborted')
53
+ acc.terminalEvent = event
54
+ acc.build = buildReducer.advance(acc.build, [event])
55
+ acc.deferral = currentDeferralObservationReducer.advance(acc.deferral, [event])
56
+ const decision = mergeAttentionFromFacts(buildReducer.finish(acc.build), acc.deferral)
57
+ if (decision && !acc.mergeAttentionPrs.includes(decision.pr.number))
58
+ acc.mergeAttentionPrs.push(decision.pr.number)
59
+ }
60
+ },
61
+ finish: (acc) => structuredClone(acc),
62
+ })
63
+
64
+ const parts = {
65
+ infrastructureReset: infrastructureFailureResetReducer,
66
+ summaryRequests: summaryRequestsReducer,
67
+ summaryBoundary: summaryBoundaryReducer,
68
+ hostedImages: hostedPrAttachmentsReducer,
69
+ designations: currentPrAttachmentsReducer,
70
+ imageHost: frozenPrImageHostReducer,
71
+ pendingAttachments: pendingPrAttachmentReclaimsReducer,
72
+ recoveryCheckpoint: recoveryCheckpointReducer,
73
+ provisionMarker: provisionMarkerReducer,
74
+ baseBranch: baseBranchReducer,
75
+ build: buildReducer,
76
+ log: logIndexReducer,
77
+ publication: publicationStateReducer,
78
+ executionNoProgress: executionNoProgressReducer,
79
+ open: openExecutionReducer,
80
+ workspace: openBuildWorkspaceReducer,
81
+ lastExecution: lastExecutionOutcomeReducer,
82
+ dashboard: dashboardFactsReducer,
83
+ deferral: currentDeferralObservationReducer,
84
+ }
85
+ export interface BuildReadAcc {
86
+ summaryRequests: ReturnType<typeof summaryRequestsReducer.initial>
87
+ summaryBoundary: ReturnType<typeof summaryBoundaryReducer.initial>
88
+ hostedImages: ReturnType<typeof hostedPrAttachmentsReducer.initial>
89
+ designations: ReturnType<typeof currentPrAttachmentsReducer.initial>
90
+ imageHost: ReturnType<typeof frozenPrImageHostReducer.initial>
91
+ pendingAttachments: ReturnType<typeof pendingPrAttachmentReclaimsReducer.initial>
92
+ recoveryCheckpoint: ReturnType<typeof recoveryCheckpointReducer.initial>
93
+ provisionMarker: ReturnType<typeof provisionMarkerReducer.initial>
94
+ baseBranch: ReturnType<typeof baseBranchReducer.initial>
95
+ build: ReturnType<typeof buildReducer.initial>
96
+ log: ReturnType<typeof logIndexReducer.initial>
97
+ publication: ReturnType<typeof publicationStateReducer.initial>
98
+ executionNoProgress: ReturnType<typeof executionNoProgressReducer.initial>
99
+ open: ReturnType<typeof openExecutionReducer.initial>
100
+ workspace: ReturnType<typeof openBuildWorkspaceReducer.initial>
101
+ lastExecution: ReturnType<typeof lastExecutionOutcomeReducer.initial>
102
+ dashboard: ReturnType<typeof dashboardFactsReducer.initial>
103
+ deferral: ReturnType<typeof currentDeferralObservationReducer.initial>
104
+ summaryHistory: {
105
+ kind: 'review' | 'verify'
106
+ phase?: string
107
+ round?: number
108
+ verdict?: string
109
+ count?: number
110
+ result?: import('../events/payloads').EventPayload<'verify.completed'>
111
+ }[]
112
+ summaryAttempts: number[]
113
+ summaryRequested: Record<string, Extract<AbEvent, { type: 'pr-summary.requested' }>>
114
+ summaryEligibleAfter?: string
115
+ finalizePublicationRequested: boolean
116
+ latestWorkspace?: Extract<AbEvent, { type: 'workspace.provisioned' }>
117
+ findings: Record<string, import('../ontology').Finding>
118
+ planFeedback: Record<string, import('../ontology').Feedback | null>
119
+ feedback: Record<string, import('../ontology').Feedback | null>
120
+ reconcileBases: Record<string, string>
121
+ firstWorkspace?: Extract<AbEvent, { type: 'workspace.provisioned' }>
122
+ latestAbort?: Extract<AbEvent, { type: 'build.abort-requested' }>
123
+ latestDiscard?: Extract<AbEvent, { type: 'build.discard-requested' }>
124
+ maxPlanRevision: number
125
+ created?: Extract<AbEvent, { type: 'build.created' }>
126
+ lastCreated?: Extract<AbEvent, { type: 'build.created' }>
127
+ latestRelease?: Extract<AbEvent, { type: 'workspace.released' }>
128
+ latestWorkspaceFailureSeq: number
129
+ workspaceGenerations: number
130
+ dispatchFailureCount: number
131
+ commentPosted: boolean
132
+ specRecorded: boolean
133
+ autoMergeEverRequested: boolean
134
+ seenTypes: AbEvent['type'][]
135
+ localWorkspace: boolean
136
+ cleanupCounts: Record<string, number>
137
+ reclaimCounts: Record<string, number>
138
+ infrastructureReset: ReturnType<typeof infrastructureFailureResetReducer.initial>
139
+ infrastructureCount: number
140
+ infrastructureRaisedSeq: number
141
+ remoteWorkspace: boolean
142
+ terminalEvent?: AbEvent
143
+ mergeTimes: string[]
144
+ observationFacts: {
145
+ payload: import('../events/payloads').EventPayload<'observation.recorded'>
146
+ seq: number
147
+ ts: string
148
+ }[]
149
+ observations: Extract<AbEvent, { type: 'observation.recorded' }>['payload'][]
150
+ }
151
+ export const buildReadFactsReducer = defineReducer({
152
+ version: 1 + Object.values(parts).reduce((sum, reducer) => sum + reducer.version, 0),
153
+ initial: (): BuildReadAcc => ({
154
+ summaryRequests: summaryRequestsReducer.initial(),
155
+ summaryBoundary: summaryBoundaryReducer.initial(),
156
+ hostedImages: hostedPrAttachmentsReducer.initial(),
157
+ designations: currentPrAttachmentsReducer.initial(),
158
+ imageHost: frozenPrImageHostReducer.initial(),
159
+ pendingAttachments: pendingPrAttachmentReclaimsReducer.initial(),
160
+ recoveryCheckpoint: recoveryCheckpointReducer.initial(),
161
+ provisionMarker: provisionMarkerReducer.initial(),
162
+ baseBranch: baseBranchReducer.initial(),
163
+ build: buildReducer.initial(),
164
+ log: logIndexReducer.initial(),
165
+ publication: publicationStateReducer.initial(),
166
+ executionNoProgress: executionNoProgressReducer.initial(),
167
+ open: openExecutionReducer.initial(),
168
+ workspace: openBuildWorkspaceReducer.initial(),
169
+ lastExecution: lastExecutionOutcomeReducer.initial(),
170
+ dashboard: dashboardFactsReducer.initial(),
171
+ deferral: currentDeferralObservationReducer.initial(),
172
+ observations: [],
173
+ observationFacts: [],
174
+ mergeTimes: [],
175
+ summaryHistory: [],
176
+ summaryAttempts: [],
177
+ summaryRequested: {},
178
+ finalizePublicationRequested: false,
179
+ findings: {},
180
+ feedback: {},
181
+ planFeedback: {},
182
+ reconcileBases: {},
183
+ maxPlanRevision: -1,
184
+ seenTypes: [],
185
+ localWorkspace: false,
186
+ cleanupCounts: {},
187
+ reclaimCounts: {},
188
+ infrastructureReset: infrastructureFailureResetReducer.initial(),
189
+ infrastructureCount: 0,
190
+ infrastructureRaisedSeq: 0,
191
+ remoteWorkspace: false,
192
+ latestWorkspaceFailureSeq: 0,
193
+ workspaceGenerations: 0,
194
+ dispatchFailureCount: 0,
195
+ commentPosted: false,
196
+ specRecorded: false,
197
+ autoMergeEverRequested: false,
198
+ }),
199
+ fold(acc: BuildReadAcc, events: readonly AbEvent[]) {
200
+ acc.baseBranch = baseBranchReducer.advance(acc.baseBranch, events)
201
+ acc.provisionMarker = provisionMarkerReducer.advance(acc.provisionMarker, events)
202
+ acc.recoveryCheckpoint = recoveryCheckpointReducer.advance(acc.recoveryCheckpoint, events)
203
+ acc.pendingAttachments = pendingPrAttachmentReclaimsReducer.advance(
204
+ acc.pendingAttachments,
205
+ events,
206
+ )
207
+ acc.imageHost = frozenPrImageHostReducer.advance(acc.imageHost, events)
208
+ acc.designations = currentPrAttachmentsReducer.advance(acc.designations, events)
209
+ acc.hostedImages = hostedPrAttachmentsReducer.advance(acc.hostedImages, events)
210
+ acc.summaryBoundary = summaryBoundaryReducer.advance(acc.summaryBoundary, events)
211
+ acc.summaryRequests = summaryRequestsReducer.advance(acc.summaryRequests, events)
212
+ acc.build = buildReducer.advance(acc.build, events)
213
+ acc.log = logIndexReducer.advance(acc.log, events)
214
+ acc.publication = publicationStateReducer.advance(acc.publication, events)
215
+ acc.executionNoProgress = executionNoProgressReducer.advance(acc.executionNoProgress, events)
216
+ acc.open = openExecutionReducer.advance(acc.open, events)
217
+ acc.workspace = openBuildWorkspaceReducer.advance(acc.workspace, events)
218
+ acc.lastExecution = lastExecutionOutcomeReducer.advance(acc.lastExecution, events)
219
+ acc.dashboard = dashboardFactsReducer.advance(acc.dashboard, events)
220
+ acc.deferral = currentDeferralObservationReducer.advance(acc.deferral, events)
221
+ for (const event of events) {
222
+ if (event.type === 'plan-review.verdict' || event.type === 'code-review.verdict')
223
+ acc.summaryHistory.push({
224
+ kind: 'review',
225
+ phase: event.type === 'plan-review.verdict' ? 'plan-review' : 'code-review',
226
+ round: event.payload.round,
227
+ verdict: event.payload.verdict,
228
+ count: event.payload.findings.length,
229
+ })
230
+ if (event.type === 'verify.completed')
231
+ acc.summaryHistory.push({ kind: 'verify', result: event.payload })
232
+ if (
233
+ event.type === 'pr-summary.attempted' &&
234
+ !acc.summaryAttempts.includes(event.payload.occurrence)
235
+ )
236
+ acc.summaryAttempts.push(event.payload.occurrence)
237
+ if (event.type === 'pr-summary.requested')
238
+ acc.summaryRequested[String(event.payload.occurrence)] ??= event
239
+ if (
240
+ event.type === 'pr-summary.result' &&
241
+ event.payload.eligibleAfter &&
242
+ (!acc.summaryEligibleAfter ||
243
+ Date.parse(event.payload.eligibleAfter) > Date.parse(acc.summaryEligibleAfter))
244
+ )
245
+ acc.summaryEligibleAfter = event.payload.eligibleAfter
246
+ if (event.type === 'publication.requested' && event.payload.operation === 'finalize')
247
+ acc.finalizePublicationRequested = true
248
+ if (event.type === 'workspace.provisioned') acc.latestWorkspace = event
249
+ if (event.type === 'plan-review.verdict')
250
+ acc.planFeedback[String(event.payload.round + 1)] =
251
+ event.payload.verdict === 'revise'
252
+ ? { findings: event.payload.findings.map((finding) => finding.id) }
253
+ : null
254
+ if (event.type === 'plan-review.verdict' || event.type === 'code-review.verdict')
255
+ for (const finding of event.payload.findings)
256
+ acc.findings[JSON.stringify(finding.id)] = finding
257
+ if (event.type === 'plan.started' || event.type === 'implement.started')
258
+ acc.feedback[
259
+ JSON.stringify([
260
+ event.type === 'plan.started' ? 'plan' : 'implement',
261
+ event.payload.round,
262
+ ])
263
+ ] = event.payload.feedback ?? null
264
+ if (event.type === 'verify.started')
265
+ acc.feedback[JSON.stringify([`verify:${event.payload.step}`, event.payload.attempt])] =
266
+ event.payload.feedback ?? null
267
+ if (event.type === 'reconcile.started')
268
+ acc.reconcileBases[String(event.payload.attempt)] = event.payload.baseSha
269
+ if (event.type === 'workspace.provisioned') acc.firstWorkspace ??= event
270
+ if (event.type === 'build.abort-requested') acc.latestAbort = event
271
+ if (event.type === 'build.discard-requested') acc.latestDiscard = event
272
+ if (event.type === 'plan.completed')
273
+ acc.maxPlanRevision = Math.max(acc.maxPlanRevision, event.payload.artifact.rev)
274
+ if (!acc.seenTypes.includes(event.type)) acc.seenTypes.push(event.type)
275
+ if (event.type === 'workspace.provisioned' && !isRemoteWorkspace(event.payload))
276
+ acc.localWorkspace = true
277
+ if (event.type === 'infrastructure.cleanup-attempted') {
278
+ const key = JSON.stringify(event.payload.workspaceRef)
279
+ acc.cleanupCounts[key] = (acc.cleanupCounts[key] ?? 0) + 1
280
+ }
281
+ if (event.type === 'pr-attachment.reclaim-failed') {
282
+ const key = String(event.payload.hostedSeq)
283
+ acc.reclaimCounts[key] = (acc.reclaimCounts[key] ?? 0) + 1
284
+ }
285
+ const priorReset = infrastructureFailureResetReducer.finish(acc.infrastructureReset)
286
+ acc.infrastructureReset = infrastructureFailureResetReducer.advance(acc.infrastructureReset, [
287
+ event,
288
+ ])
289
+ if (infrastructureFailureResetReducer.finish(acc.infrastructureReset) !== priorReset)
290
+ acc.infrastructureCount = 0
291
+ if (event.type === 'infrastructure.failed') acc.infrastructureCount++
292
+ if (
293
+ event.type === 'escalation.raised' &&
294
+ event.payload.policyCause === 'infrastructure-failure-limit'
295
+ )
296
+ acc.infrastructureRaisedSeq = event.seq
297
+ if (event.type === 'build.created') {
298
+ acc.created ??= event
299
+ acc.lastCreated = event
300
+ }
301
+ if (event.type === 'workspace.provisioned') acc.workspaceGenerations++
302
+ if (event.type === 'workspace.released') acc.latestRelease = event
303
+ if (event.type === 'dispatch.failed') {
304
+ acc.dispatchFailureCount++
305
+ if (event.payload.stage === 'workspace') acc.latestWorkspaceFailureSeq = event.seq
306
+ }
307
+ if (event.type === 'dispatch.comment-posted') acc.commentPosted = true
308
+ if (event.type === 'spec.imported' || event.type === 'spec.authored') acc.specRecorded = true
309
+ if (event.type === 'build.auto-merge-requested') acc.autoMergeEverRequested = true
310
+ if (event.type === 'workspace.provisioned' && isRemoteWorkspace(event.payload))
311
+ acc.remoteWorkspace = true
312
+ if (event.type === 'build.completed' || event.type === 'build.aborted')
313
+ acc.terminalEvent = event
314
+ if (event.type === 'observation.recorded') {
315
+ acc.observations.push(event.payload)
316
+ acc.observationFacts.push({ payload: event.payload, seq: event.seq, ts: event.ts })
317
+ }
318
+ if (event.type === 'pr.merged') acc.mergeTimes.push(event.ts)
319
+ }
320
+ },
321
+ finish: (acc: BuildReadAcc) => ({
322
+ summaryRequests: summaryRequestsReducer.finish(acc.summaryRequests),
323
+ summaryBoundary: summaryBoundaryReducer.finish(acc.summaryBoundary),
324
+ hostedImages: [...hostedPrAttachmentsReducer.finish(acc.hostedImages)],
325
+ designations: currentPrAttachmentsReducer.finish(acc.designations),
326
+ imageHost: frozenPrImageHostReducer.finish(acc.imageHost),
327
+ pendingAttachments: pendingPrAttachmentReclaimsReducer.finish(acc.pendingAttachments),
328
+ recoveryCheckpoint: recoveryCheckpointReducer.finish(acc.recoveryCheckpoint),
329
+ provisionMarker: provisionMarkerReducer.finish(acc.provisionMarker),
330
+ baseBranch: baseBranchReducer.finish(acc.baseBranch),
331
+ state: buildReducer.finish(acc.build),
332
+ log: encodeLog(logIndexReducer.finish(acc.log)),
333
+ publication: structuredClone(acc.publication),
334
+ executionNoProgress: executionNoProgressReducer.finish(acc.executionNoProgress),
335
+ open: openExecutionReducer.finish(acc.open),
336
+ workspace: openBuildWorkspaceReducer.finish(acc.workspace),
337
+ lastExecution: lastExecutionOutcomeReducer.finish(acc.lastExecution),
338
+ dashboard: dashboardFactsReducer.finish(acc.dashboard),
339
+ deferral: currentDeferralObservationReducer.finish(acc.deferral),
340
+ observationFacts: structuredClone(acc.observationFacts),
341
+ mergeTimes: [...acc.mergeTimes],
342
+ observations: structuredClone(acc.observations),
343
+ terminalEvent: acc.terminalEvent,
344
+ summaryHistory: structuredClone(acc.summaryHistory),
345
+ summaryAttempts: [...acc.summaryAttempts],
346
+ summaryRequested: structuredClone(acc.summaryRequested),
347
+ summaryEligibleAfter: acc.summaryEligibleAfter,
348
+ finalizePublicationRequested: acc.finalizePublicationRequested,
349
+ latestWorkspace: acc.latestWorkspace,
350
+ planFeedback: structuredClone(acc.planFeedback),
351
+ findings: structuredClone(acc.findings),
352
+ feedback: structuredClone(acc.feedback),
353
+ reconcileBases: { ...acc.reconcileBases },
354
+ firstWorkspace: acc.firstWorkspace,
355
+ latestAbort: acc.latestAbort,
356
+ latestDiscard: acc.latestDiscard,
357
+ maxPlanRevision: acc.maxPlanRevision,
358
+ seenTypes: [...acc.seenTypes],
359
+ localWorkspace: acc.localWorkspace,
360
+ cleanupCounts: { ...acc.cleanupCounts },
361
+ reclaimCounts: { ...acc.reclaimCounts },
362
+ infrastructureReset: infrastructureFailureResetReducer.finish(acc.infrastructureReset),
363
+ infrastructureCount: acc.infrastructureCount,
364
+ infrastructureRaisedSeq: acc.infrastructureRaisedSeq,
365
+ remoteWorkspace: acc.remoteWorkspace,
366
+ created: acc.created,
367
+ lastCreated: acc.lastCreated,
368
+ latestRelease: acc.latestRelease,
369
+ latestWorkspaceFailureSeq: acc.latestWorkspaceFailureSeq,
370
+ workspaceGenerations: acc.workspaceGenerations,
371
+ dispatchFailureCount: acc.dispatchFailureCount,
372
+ commentPosted: acc.commentPosted,
373
+ specRecorded: acc.specRecorded,
374
+ autoMergeEverRequested: acc.autoMergeEverRequested,
375
+ }),
376
+ })
377
+
378
+ import type { RepositoryEvent } from '../events/repository'
379
+ import { repositoryAttentionReducer } from './repository-attention'
380
+ import { dispatchSettingsReducer } from './dispatch-settings'
381
+ import { harvestReducer } from './harvest'
382
+ import { ticketSourceHealthReducer } from './ticket-source-health'
383
+ import { autoMergeDefaultReducer } from './auto-merge-default'
384
+ import { effectiveConfigReducer } from './service-facts'
385
+ import { sandboxStatesReducer } from '../processes/sandbox-state'
386
+ import { openHarvestExecutionsReducer } from '../processes/harvest-execution-state'
387
+ import { dispatchStatusReducer } from './dispatch-status'
388
+
389
+ /** Shared current reads retain a fixed recent window. Historical holder
390
+ * correlations outside it use the explicitly parameterized cache. */
391
+ export const RECENT_DISPATCH_RUN_LIMIT = 16
392
+ export interface RepositoryReadAcc {
393
+ openHarvest: ReturnType<typeof openHarvestExecutionsReducer.initial>
394
+ attention: ReturnType<typeof repositoryAttentionReducer.initial>
395
+ harvestFailures: Record<
396
+ string,
397
+ { count: number; latest: Extract<RepositoryEvent, { type: 'harvest.failed' }> }
398
+ >
399
+ harvestVerdicts: Record<
400
+ string,
401
+ {
402
+ first: Extract<RepositoryEvent, { type: 'harvest.review.verdict' }>
403
+ latest: Extract<RepositoryEvent, { type: 'harvest.review.verdict' }>
404
+ }
405
+ >
406
+ harvestAuthorizations: Record<
407
+ string,
408
+ Extract<RepositoryEvent, { type: 'harvest.execution.authorized' }>
409
+ >
410
+ harvestExecutionStarts: Record<string, string>
411
+ latestHarvestResumedSeq: number
412
+ harvestSessionEndings: Record<string, number>
413
+ harvestSessions: (import('../cli/harvest').HarvestSessionStatusView & { run: string })[]
414
+ pendingHumanResume?: number
415
+ dismissedThrough: number
416
+ failedAt: Record<string, string>
417
+ settings: ReturnType<typeof dispatchSettingsReducer.initial>
418
+ harvest: ReturnType<typeof harvestReducer.initial>
419
+ health: ReturnType<typeof ticketSourceHealthReducer.initial>
420
+ autoMerge: ReturnType<typeof autoMergeDefaultReducer.initial>
421
+ config: ReturnType<typeof effectiveConfigReducer.initial>
422
+ sandboxes: ReturnType<typeof sandboxStatesReducer.initial>
423
+ status: ReturnType<ReturnType<typeof dispatchStatusReducer>['initial']>
424
+ run?: string
425
+ scan?: Extract<RepositoryEvent, { type: 'dispatcher.ticket-scan-completed' }>['payload']
426
+ recentRuns: [string, DispatcherRunFacts][]
427
+ latestRun?: string
428
+ latestStoppedRun?: string
429
+ publications: Extract<RepositoryEvent, { type: 'orchestrator.sandbox.published' }>[]
430
+ }
431
+ export const repositoryReadFactsReducer = defineReducer({
432
+ version:
433
+ 1 +
434
+ dispatcherRunFactsReducer('version').version +
435
+ openHarvestExecutionsReducer.version +
436
+ repositoryAttentionReducer.version +
437
+ dispatchSettingsReducer.version +
438
+ harvestReducer.version +
439
+ ticketSourceHealthReducer.version +
440
+ autoMergeDefaultReducer.version +
441
+ effectiveConfigReducer.version +
442
+ sandboxStatesReducer.version +
443
+ dispatchStatusReducer('version').version,
444
+ initial: (): RepositoryReadAcc => ({
445
+ recentRuns: [],
446
+ openHarvest: openHarvestExecutionsReducer.initial(),
447
+ attention: repositoryAttentionReducer.initial(),
448
+ dismissedThrough: 0,
449
+ failedAt: {},
450
+ harvestSessions: [],
451
+ harvestSessionEndings: {},
452
+ harvestFailures: {},
453
+ harvestVerdicts: {},
454
+ harvestAuthorizations: {},
455
+ latestHarvestResumedSeq: 0,
456
+ harvestExecutionStarts: {},
457
+ settings: dispatchSettingsReducer.initial(),
458
+ harvest: harvestReducer.initial(),
459
+ health: ticketSourceHealthReducer.initial(),
460
+ autoMerge: autoMergeDefaultReducer.initial(),
461
+ config: effectiveConfigReducer.initial(),
462
+ sandboxes: sandboxStatesReducer.initial(),
463
+ status: dispatchStatusReducer('initial').initial(),
464
+ publications: [],
465
+ }),
466
+ fold(acc: RepositoryReadAcc, events: readonly RepositoryEvent[]) {
467
+ acc.attention = repositoryAttentionReducer.advance(acc.attention, events)
468
+ acc.openHarvest = openHarvestExecutionsReducer.advance(acc.openHarvest, events)
469
+ acc.settings = dispatchSettingsReducer.advance(acc.settings, events)
470
+ acc.harvest = harvestReducer.advance(acc.harvest, events)
471
+ acc.health = ticketSourceHealthReducer.advance(acc.health, events)
472
+ acc.autoMerge = autoMergeDefaultReducer.advance(acc.autoMerge, events)
473
+ acc.config = effectiveConfigReducer.advance(acc.config, events)
474
+ acc.sandboxes = sandboxStatesReducer.advance(acc.sandboxes, events)
475
+ for (const event of events) {
476
+ if (event.type === 'harvest.failed') {
477
+ const key = JSON.stringify([event.payload.run, event.payload.step, event.payload.round])
478
+ acc.harvestFailures[key] = {
479
+ count: (acc.harvestFailures[key]?.count ?? 0) + 1,
480
+ latest: event,
481
+ }
482
+ }
483
+ if (event.type === 'harvest.review.verdict') {
484
+ const key = JSON.stringify([event.payload.run, event.payload.round])
485
+ acc.harvestVerdicts[key] = {
486
+ first: acc.harvestVerdicts[key]?.first ?? event,
487
+ latest: event,
488
+ }
489
+ }
490
+ if (event.type === 'harvest.execution.started')
491
+ acc.harvestExecutionStarts[String(event.seq)] = event.payload.execution
492
+ if (event.type === 'harvest.execution.authorized')
493
+ acc.harvestAuthorizations[JSON.stringify(event.payload.execution)] ??= event
494
+ if (event.type === 'harvest.resumed') acc.latestHarvestResumedSeq = event.seq
495
+ if (event.type === 'harvest.session.started')
496
+ acc.harvestSessions.push({
497
+ run: event.payload.run,
498
+ session: event.payload.session,
499
+ role: event.payload.role,
500
+ step: event.payload.step,
501
+ round: event.payload.round,
502
+ status: 'open',
503
+ ...(event.payload.stream === undefined ? {} : { stream: event.payload.stream }),
504
+ streamStatus: event.payload.stream === undefined ? 'closed' : 'open',
505
+ })
506
+ if (event.type === 'harvest.session.ended') {
507
+ acc.harvestSessionEndings[JSON.stringify(event.payload.session)] ??= event.seq
508
+ const session = acc.harvestSessions.findLast(
509
+ (session) => session.session === event.payload.session && session.status === 'open',
510
+ )
511
+ if (session) {
512
+ session.status = 'ended'
513
+ session.streamStatus = 'closed'
514
+ }
515
+ }
516
+ if (event.type === 'harvest.resume-requested' && event.actor.kind === 'human')
517
+ acc.pendingHumanResume = event.seq
518
+ if (event.type === 'harvest.pause-requested') delete acc.pendingHumanResume
519
+ if (event.type === 'harvest.resumed' && acc.pendingHumanResume !== undefined)
520
+ acc.dismissedThrough = Math.max(acc.dismissedThrough, acc.pendingHumanResume)
521
+ if (event.type === 'harvest.failed' && !event.payload.willRetry)
522
+ acc.failedAt[JSON.stringify(event.payload.run)] = event.ts
523
+ if (event.type === 'dispatcher.run-started') {
524
+ acc.run = event.payload.run
525
+ acc.status = dispatchStatusReducer(acc.run).initial()
526
+ }
527
+ if (acc.run) acc.status = dispatchStatusReducer(acc.run).advance(acc.status, [event])
528
+ if (event.type === 'dispatcher.ticket-scan-completed') acc.scan = event.payload
529
+ if (event.type === 'dispatcher.run-started') acc.latestRun = event.payload.run
530
+ if (event.type === 'dispatcher.run-stopped') acc.latestStoppedRun = event.payload.run
531
+ if (
532
+ event.type.startsWith('dispatcher.') &&
533
+ 'run' in event.payload &&
534
+ typeof event.payload.run === 'string'
535
+ ) {
536
+ const run = event.payload.run
537
+ let entry = acc.recentRuns.find(([id]) => id === run)
538
+ if (!entry) {
539
+ entry = [run, {}]
540
+ acc.recentRuns.push(entry)
541
+ }
542
+ foldDispatcherRunFact(entry[1], event)
543
+ while (acc.recentRuns.length > RECENT_DISPATCH_RUN_LIMIT) {
544
+ const index = acc.recentRuns.findIndex(
545
+ ([id]) => id !== acc.latestRun && id !== acc.latestStoppedRun,
546
+ )
547
+ acc.recentRuns.splice(index, 1)
548
+ }
549
+ }
550
+ if (event.type === 'orchestrator.sandbox.published') {
551
+ acc.publications = acc.publications.filter(
552
+ (prior) =>
553
+ prior.payload.operator !== event.payload.operator ||
554
+ prior.payload.branch !== event.payload.branch,
555
+ )
556
+ acc.publications.push(event)
557
+ }
558
+ }
559
+ },
560
+ finish: (acc: RepositoryReadAcc) => ({
561
+ harvestFailures: structuredClone(acc.harvestFailures),
562
+ harvestVerdicts: structuredClone(acc.harvestVerdicts),
563
+ harvestAuthorizations: structuredClone(acc.harvestAuthorizations),
564
+ harvestExecutionStarts: { ...acc.harvestExecutionStarts },
565
+ latestHarvestResumedSeq: acc.latestHarvestResumedSeq,
566
+ harvestSessionEndings: { ...acc.harvestSessionEndings },
567
+ harvestSessions: structuredClone(acc.harvestSessions),
568
+ attention: repositoryAttentionReducer.finish(acc.attention),
569
+ dismissedThrough: acc.dismissedThrough,
570
+ failedAt: { ...acc.failedAt },
571
+ openHarvest: openHarvestExecutionsReducer.finish(acc.openHarvest),
572
+ settings: dispatchSettingsReducer.finish(acc.settings),
573
+ harvest: harvestReducer.finish(acc.harvest),
574
+ health: ticketSourceHealthReducer.finish(acc.health),
575
+ autoMerge: autoMergeDefaultReducer.finish(acc.autoMerge),
576
+ config: effectiveConfigReducer.finish(acc.config),
577
+ sandboxes: sandboxStatesReducer.finish(acc.sandboxes),
578
+ status: acc.run ? dispatchStatusReducer(acc.run).finish(acc.status) : undefined,
579
+ scan: acc.scan,
580
+ recentRuns: structuredClone(acc.recentRuns),
581
+ latestRun: acc.latestRun,
582
+ latestStoppedRun: acc.latestStoppedRun,
583
+ publications: structuredClone(acc.publications),
584
+ }),
585
+ })
586
+
587
+ import type { IncrementalReducer } from './incremental'
588
+ /** Companion for an optional finished answer: absence is represented by an
589
+ * omitted object field, never top-level undefined. */
590
+ export function optionalValue<A, E, V>(reducer: IncrementalReducer<A, E, V>) {
591
+ return {
592
+ ...reducer,
593
+ finish: (acc: A) => ({ value: reducer.finish(acc) }),
594
+ reduce: (events: readonly E[]) => ({ value: reducer.reduce(events) }),
595
+ }
596
+ }
597
+
598
+ /** One run's correlation facts. History for unrelated runs never enters this value. */
599
+ export interface DispatcherRunFacts {
600
+ start?: RepositoryEvent
601
+ latestStart?: RepositoryEvent
602
+ stop?: RepositoryEvent
603
+ tick?: RepositoryEvent
604
+ completed?: RepositoryEvent
605
+ boundary?: 'dispatcher.tick-started' | 'dispatcher.tick-completed' | 'dispatcher.tick-failed'
606
+ stopRequest?: Extract<RepositoryEvent, { type: 'dispatcher.stop-requested' }>
607
+ }
608
+ export function dispatcherRunFactsReducer(run: string) {
609
+ return defineReducer<DispatcherRunFacts, RepositoryEvent, DispatcherRunFacts>({
610
+ version: 2,
611
+ initial: () => ({}),
612
+ fold(acc, events) {
613
+ for (const event of events) {
614
+ if (!('run' in event.payload) || event.payload.run !== run) continue
615
+ foldDispatcherRunFact(acc, event)
616
+ }
617
+ },
618
+ finish: (acc) => structuredClone(acc),
619
+ })
620
+ }
621
+
622
+ function foldDispatcherRunFact(acc: DispatcherRunFacts, event: RepositoryEvent): void {
623
+ if (event.type === 'dispatcher.run-started') {
624
+ acc.start ??= event
625
+ acc.latestStart = event
626
+ } else if (event.type === 'dispatcher.run-stopped') acc.stop = event
627
+ else if (event.type === 'dispatcher.stop-requested') acc.stopRequest ??= event
628
+ else if (
629
+ event.type === 'dispatcher.tick-started' ||
630
+ event.type === 'dispatcher.tick-completed' ||
631
+ event.type === 'dispatcher.tick-failed'
632
+ ) {
633
+ acc.boundary = event.type
634
+ if (event.type !== 'dispatcher.tick-started') acc.tick = event
635
+ if (event.type === 'dispatcher.tick-completed') acc.completed = event
636
+ }
637
+ }