@defrex/autobuild 0.12.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 (151) hide show
  1. package/README.md +1 -1
  2. package/SPEC.md +138 -16
  3. package/bin/ab-dispatch-kernel.ts +4 -6
  4. package/docs/architecture.md +155 -73
  5. package/docs/configuration.md +41 -9
  6. package/docs/hosted-service.md +31 -5
  7. package/docs/operator-api.md +8 -1
  8. package/docs/remote-store-protocol.md +94 -2
  9. package/docs/remote-ticket-protocol.md +151 -9
  10. package/docs/setup.md +33 -7
  11. package/docs/ticket-migration.md +64 -0
  12. package/package.json +4 -3
  13. package/packages/core/src/cli/attention.ts +18 -164
  14. package/packages/core/src/cli/build-control.ts +23 -16
  15. package/packages/core/src/cli/bulk-control.ts +3 -2
  16. package/packages/core/src/cli/connect.ts +141 -38
  17. package/packages/core/src/cli/context.ts +23 -79
  18. package/packages/core/src/cli/dashboard/model.ts +36 -22
  19. package/packages/core/src/cli/dashboard/poll.ts +1 -1
  20. package/packages/core/src/cli/dispatch-frontend.ts +24 -67
  21. package/packages/core/src/cli/dispatch-process.ts +14 -7
  22. package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
  23. package/packages/core/src/cli/dispatch.ts +304 -82
  24. package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
  25. package/packages/core/src/cli/harvest.ts +84 -42
  26. package/packages/core/src/cli/help.ts +14 -8
  27. package/packages/core/src/cli/historical-build-state.ts +27 -0
  28. package/packages/core/src/cli/installation.ts +67 -13
  29. package/packages/core/src/cli/main.ts +23 -5
  30. package/packages/core/src/cli/mcp.ts +2 -1
  31. package/packages/core/src/cli/pr-attachments.ts +10 -13
  32. package/packages/core/src/cli/pr-summary-publication.ts +27 -60
  33. package/packages/core/src/cli/pr-summary.ts +43 -13
  34. package/packages/core/src/cli/publication-settlement.ts +10 -20
  35. package/packages/core/src/cli/repository-status.ts +38 -2
  36. package/packages/core/src/cli/self-update.ts +5 -1
  37. package/packages/core/src/cli/status.ts +62 -32
  38. package/packages/core/src/cli/terminals.ts +25 -142
  39. package/packages/core/src/cli/ticket-assets.ts +7 -2
  40. package/packages/core/src/cli/ticket-editor.ts +82 -0
  41. package/packages/core/src/cli/ticket.ts +166 -36
  42. package/packages/core/src/cli/wait.ts +91 -84
  43. package/packages/core/src/cli/watch.ts +60 -97
  44. package/packages/core/src/config/pipeline-source.ts +4 -3
  45. package/packages/core/src/config/schema.ts +35 -3
  46. package/packages/core/src/connection.ts +3 -3
  47. package/packages/core/src/events/catalog.ts +3 -2
  48. package/packages/core/src/events/payloads.ts +6 -0
  49. package/packages/core/src/events/repository.ts +7 -3
  50. package/packages/core/src/integration/harness.ts +38 -4
  51. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2444 -9
  52. package/packages/core/src/kernel/auto-merge.ts +19 -3
  53. package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
  54. package/packages/core/src/kernel/generators/build-log.ts +1 -1
  55. package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
  56. package/packages/core/src/kernel/harvest.ts +25 -23
  57. package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
  58. package/packages/core/src/kernel/log-index-value.ts +38 -0
  59. package/packages/core/src/kernel/read-facts.ts +637 -0
  60. package/packages/core/src/kernel/reducer-registry.ts +139 -3
  61. package/packages/core/src/kernel/repository-attention.ts +179 -0
  62. package/packages/core/src/kernel/service-facts.ts +135 -0
  63. package/packages/core/src/kernel/terminal-guard.ts +133 -0
  64. package/packages/core/src/ontology.ts +1 -0
  65. package/packages/core/src/operator/annotations.ts +12 -0
  66. package/packages/core/src/operator/control.ts +5 -6
  67. package/packages/core/src/operator/index.ts +7 -0
  68. package/packages/core/src/operator/protocol.ts +19 -2
  69. package/packages/core/src/operator/query.ts +37 -20
  70. package/packages/core/src/operator/read-view.ts +4 -13
  71. package/packages/core/src/operator/registry.ts +61 -8
  72. package/packages/core/src/operator/requests.ts +2 -14
  73. package/packages/core/src/operator/sandbox.ts +10 -9
  74. package/packages/core/src/operator/ticket-source-backend.ts +17 -21
  75. package/packages/core/src/operator/tickets.ts +132 -22
  76. package/packages/core/src/plugin-sdk/index.ts +19 -0
  77. package/packages/core/src/plugins/runtimes.ts +5 -1
  78. package/packages/core/src/ports/runner/session-env.ts +2 -0
  79. package/packages/core/src/ports/tickets/contract.ts +66 -1
  80. package/packages/core/src/ports/tickets/create.ts +48 -12
  81. package/packages/core/src/ports/tickets/fake.ts +30 -1
  82. package/packages/core/src/ports/tickets/file-assets.ts +159 -0
  83. package/packages/core/src/ports/tickets/file-codec.ts +90 -0
  84. package/packages/core/src/ports/tickets/file.ts +114 -120
  85. package/packages/core/src/ports/tickets/grants.ts +47 -0
  86. package/packages/core/src/ports/tickets/hosted-index.ts +17 -0
  87. package/packages/core/src/ports/tickets/identity.ts +20 -0
  88. package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
  89. package/packages/core/src/ports/tickets/linear.ts +74 -9
  90. package/packages/core/src/ports/tickets/migration.ts +336 -0
  91. package/packages/core/src/ports/tickets/operations.ts +38 -7
  92. package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
  93. package/packages/core/src/ports/tickets/remote-protocol.ts +95 -0
  94. package/packages/core/src/ports/tickets/remote.ts +60 -3
  95. package/packages/core/src/ports/tickets/requested-id.ts +46 -0
  96. package/packages/core/src/ports/tickets/revision.ts +62 -0
  97. package/packages/core/src/ports/tickets/states.ts +61 -0
  98. package/packages/core/src/ports/types.ts +40 -3
  99. package/packages/core/src/ports/workspace/build-execution.ts +1 -0
  100. package/packages/core/src/ports/workspace/create.ts +5 -1
  101. package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
  102. package/packages/core/src/ports/workspace/vercel-sandbox.ts +8 -4
  103. package/packages/core/src/processes/build-child.ts +6 -6
  104. package/packages/core/src/processes/build-runner.ts +62 -70
  105. package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
  106. package/packages/core/src/processes/dispatcher.ts +223 -205
  107. package/packages/core/src/processes/execution-no-progress.ts +176 -0
  108. package/packages/core/src/processes/harvest-child.ts +30 -4
  109. package/packages/core/src/processes/harvest-execution-state.ts +15 -9
  110. package/packages/core/src/processes/harvest-runner.ts +58 -74
  111. package/packages/core/src/processes/harvest.ts +72 -29
  112. package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
  113. package/packages/core/src/processes/orchestrator-tick.ts +2 -2
  114. package/packages/core/src/processes/phase-failure-budget.ts +1 -0
  115. package/packages/core/src/processes/publication-loss.ts +35 -21
  116. package/packages/core/src/processes/repo-view.ts +229 -1099
  117. package/packages/core/src/processes/tick-publication.ts +110 -0
  118. package/packages/core/src/store/adapter.ts +23 -0
  119. package/packages/core/src/store/build-scope.ts +17 -0
  120. package/packages/core/src/store/contract.ts +283 -1
  121. package/packages/core/src/store/local/schema.ts +1 -0
  122. package/packages/core/src/store/local/store.ts +180 -55
  123. package/packages/core/src/store/memory.ts +119 -29
  124. package/packages/core/src/store/phase-session.ts +17 -0
  125. package/packages/core/src/store/reduced-reads.ts +239 -0
  126. package/packages/core/src/store/reducer-value-shapes.json +14056 -0
  127. package/packages/core/src/store/reducer-value-validation.ts +96 -0
  128. package/packages/core/src/store/reducer-values.ts +145 -0
  129. package/packages/core/src/store/remote/client.ts +41 -0
  130. package/packages/core/src/store/remote/compatibility.ts +2 -0
  131. package/packages/core/src/store/remote/index.ts +19 -0
  132. package/packages/core/src/store/remote/protocol.ts +16 -0
  133. package/packages/core/src/store/remote/version.ts +3 -1
  134. package/packages/core/src/store/repo-state-events.ts +1 -1
  135. package/packages/core/src/store/repo-tick.ts +72 -0
  136. package/packages/core/src/store/session-handle.ts +19 -0
  137. package/packages/core/src/store/snapshots.ts +10 -0
  138. package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
  139. package/packages/core/src/store/ticket-assets.ts +86 -8
  140. package/packages/core/src/store/types.ts +40 -3
  141. package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
  142. package/packages/core/src/testing/index.ts +8 -0
  143. package/packages/core/src/testing/reduced-read-budget.ts +462 -0
  144. package/skills/guide/SKILL.md +97 -12
  145. package/skills/guide/references/operator-api.md +8 -1
  146. package/skills/guide/references/remote-store-protocol.md +94 -2
  147. package/skills/guide/references/remote-ticket-protocol.md +151 -9
  148. package/skills/guide/references/setup.md +33 -7
  149. package/skills/guide/references/ticket-migration.md +64 -0
  150. package/skills/tickets/SKILL.md +71 -0
  151. package/templates/hosted-service/package.json +2 -2
@@ -1,3 +1,5 @@
1
+ import { readBuildDigests } from '../store/reduced-reads'
2
+ import { readRepositoryFacts, readBuildFacts } from '../store/reduced-reads'
1
3
  /** Deterministic halves of observation harvest: scan, dedup, validation, and
2
4
  * proposal rendering. Agent judgment is deliberately absent from this file. */
3
5
  import type { AbEvent } from '../events/catalog'
@@ -135,10 +137,12 @@ export function evaluateHarvestPressure(
135
137
  * so latest-wins carries exactly the drift fact the scan collects. */
136
138
  export function evaluateHarvestPressureFromDigests(input: {
137
139
  digests: Map<string, BuildDigest>
138
- harvestEvents: RepositoryEvent[]
140
+ harvestEvents: RepositoryEvent[] | HarvestState
139
141
  policy: { harvestThreshold: number; harvestMaxDrift: number }
140
142
  }): HarvestPressure {
141
- const claimed = claimedOccurrenceKeys(reduceHarvest(input.harvestEvents))
143
+ const claimed = claimedOccurrenceKeys(
144
+ Array.isArray(input.harvestEvents) ? reduceHarvest(input.harvestEvents) : input.harvestEvents,
145
+ )
142
146
  const unclaimed: HarvestPressureFacts['unclaimed'] = []
143
147
  const merges: HarvestPressureFacts['merges'] = []
144
148
  for (const digest of input.digests.values()) {
@@ -160,11 +164,11 @@ export function evaluateHarvestPressureFromDigests(input: {
160
164
  export async function evaluateHarvestPressureFromStore(input: {
161
165
  store: BuildStore
162
166
  repo: string
163
- harvestEvents: RepositoryEvent[]
167
+ harvestEvents: RepositoryEvent[] | HarvestState
164
168
  policy: { harvestThreshold: number; harvestMaxDrift: number }
165
169
  }): Promise<HarvestPressure> {
166
170
  await input.store.ensureRepo(input.repo)
167
- const digests = await input.store.getRepoBuildDigests(input.repo)
171
+ const digests = await readBuildDigests(input.store, input.repo)
168
172
  return evaluateHarvestPressureFromDigests({
169
173
  digests,
170
174
  harvestEvents: input.harvestEvents,
@@ -182,9 +186,11 @@ export function collectUnclaimedObservations(input: {
182
186
  repo: string
183
187
  records: BuildRecord[]
184
188
  eventsByBuild: Map<string, AbEvent[]>
185
- harvestEvents: RepositoryEvent[]
189
+ harvestEvents: RepositoryEvent[] | HarvestState
186
190
  }): HarvestScanResult {
187
- const state = reduceHarvest(input.harvestEvents)
191
+ const state = Array.isArray(input.harvestEvents)
192
+ ? reduceHarvest(input.harvestEvents)
193
+ : input.harvestEvents
188
194
  const claimed = claimedOccurrenceKeys(state)
189
195
  const observations: HarvestObservation[] = []
190
196
  const merges: HarvestMerge[] = []
@@ -235,9 +241,11 @@ export function collectUnclaimedObservations(input: {
235
241
  * are not assumed globally unique. */
236
242
  export function unclaimedObservationCount(input: {
237
243
  digests: Map<string, BuildDigest>
238
- harvestEvents: RepositoryEvent[]
244
+ harvestEvents: RepositoryEvent[] | HarvestState
239
245
  }): number {
240
- const claimed = claimedOccurrenceKeys(reduceHarvest(input.harvestEvents))
246
+ const claimed = claimedOccurrenceKeys(
247
+ Array.isArray(input.harvestEvents) ? reduceHarvest(input.harvestEvents) : input.harvestEvents,
248
+ )
241
249
  let count = 0
242
250
  for (const digest of input.digests.values()) {
243
251
  for (const observation of digest.observations) {
@@ -279,7 +287,7 @@ export async function sampleUnclaimedObservationCount(
279
287
  ): Promise<number> {
280
288
  const [digests, harvestEvents] = await Promise.all([
281
289
  store.getRepoBuildDigests(repo),
282
- readRepoEventsIfRecorded(store, repo),
290
+ readRepositoryFacts(store, repo).then((result) => result.value.harvest),
283
291
  ])
284
292
  return unclaimedObservationCount({ digests, harvestEvents })
285
293
  }
@@ -292,16 +300,31 @@ export async function scanUnclaimedObservations(
292
300
  repo: string,
293
301
  ): Promise<HarvestScanResult> {
294
302
  await store.ensureRepo(repo)
295
- // Bounded read (AUT-489): only harvest facts are consumed here; the
296
- // per-build history reads stay as AUT-487 left them.
297
- const harvestEvents = await store.getRepoStateEvents(repo)
303
+ const state = (await readRepositoryFacts(store, repo)).value.harvest
304
+ const claimed = claimedOccurrenceKeys(state)
298
305
  const records = await store.listBuilds()
299
- const eventsByBuild = new Map<string, AbEvent[]>()
306
+ const observations: HarvestObservation[] = []
307
+ const merges: HarvestMerge[] = []
300
308
  for (const record of records) {
301
309
  if (record.repo !== repo) continue
302
- eventsByBuild.set(record.slug, await store.getEvents(record.slug))
310
+ const facts = (await readBuildFacts(store, record.slug)).value
311
+ for (const ts of facts.mergeTimes) merges.push({ build: record.slug, ts })
312
+ for (const observation of facts.observationFacts) {
313
+ const occurrence = { build: record.slug, seq: observation.seq }
314
+ if (claimed.has(occurrenceKey(occurrence))) continue
315
+ observations.push({
316
+ occurrence,
317
+ ...observation.payload,
318
+ ts: observation.ts,
319
+ ...(record.ticket ? { ticket: structuredClone(record.ticket) } : {}),
320
+ })
321
+ }
303
322
  }
304
- return collectUnclaimedObservations({ repo, records, eventsByBuild, harvestEvents })
323
+ observations.sort(
324
+ (a, b) =>
325
+ a.occurrence.build.localeCompare(b.occurrence.build) || a.occurrence.seq - b.occurrence.seq,
326
+ )
327
+ return { state, observations, merges }
305
328
  }
306
329
 
307
330
  /** Distinct previously filed/joined proposal tickets, reconciled through the
@@ -327,12 +350,17 @@ export async function reconcileHarvestLedger(
327
350
  )
328
351
  if (entries.length === 0) return []
329
352
 
330
- const ids = entries.map((entry) => entry.ticket.id)
353
+ const ids = entries
354
+ .filter((entry) => entry.ticket.source === tickets.name)
355
+ .map((entry) => entry.ticket.id)
331
356
  const states = await tickets.dependencyStates(ids)
357
+ const byId = new Map(states.map((state) => [state.id, state]))
332
358
  const out: HarvestLedgerTicket[] = []
333
359
  for (let index = 0; index < entries.length; index += 1) {
334
360
  const entry = entries[index]!
335
- const dependency = states[index] ?? {
361
+ const dependency = (entry.ticket.source === tickets.name
362
+ ? byId.get(entry.ticket.id)
363
+ : undefined) ?? {
336
364
  id: entry.ticket.id,
337
365
  exists: false,
338
366
  resolved: false,
@@ -341,7 +369,7 @@ export async function reconcileHarvestLedger(
341
369
  const current = dependency.exists ? await tickets.get(entry.ticket.id) : null
342
370
  out.push({
343
371
  proposalKey: entry.proposalKey,
344
- ticket: structuredClone(entry.ticket),
372
+ ticket: structuredClone(current?.ref ?? entry.ticket),
345
373
  exists: dependency.exists,
346
374
  resolved: dependency.resolved,
347
375
  ...(current?.title !== undefined ? { title: current.title } : {}),
@@ -371,15 +399,25 @@ export async function reconcileOriginatingTickets(
371
399
  const states = matchingIds.length > 0 ? await tickets.dependencyStates(matchingIds) : []
372
400
  const byId = new Map(states.map((state) => [state.id, state]))
373
401
 
374
- return refs.map((ticket) => {
375
- const sourceMatches = ticket.source === tickets.name
376
- const state = sourceMatches ? byId.get(ticket.id) : undefined
377
- return {
378
- ticket,
379
- sourceMatches,
380
- exists: state?.exists ?? false,
381
- resolved: state?.resolved ?? false,
382
- }
402
+ const identity = new Set<string>()
403
+ const projected = await Promise.all(
404
+ refs.map(async (ticket) => {
405
+ const sourceMatches = ticket.source === tickets.name
406
+ const state = sourceMatches ? byId.get(ticket.id) : undefined
407
+ const current = sourceMatches && state?.exists ? await tickets.get(ticket.id) : null
408
+ return {
409
+ ticket: current?.ref ?? ticket,
410
+ sourceMatches,
411
+ exists: state?.exists ?? false,
412
+ resolved: state?.resolved ?? false,
413
+ }
414
+ }),
415
+ )
416
+ return projected.filter((entry) => {
417
+ const key = `${entry.ticket.source}:${entry.ticket.id}`
418
+ if (identity.has(key)) return false
419
+ identity.add(key)
420
+ return true
383
421
  })
384
422
  }
385
423
 
@@ -536,9 +574,14 @@ export async function resolveHarvestCreateBlockers(
536
574
  const state = byId.get(id)
537
575
  return state?.exists === true && !state.resolved
538
576
  })
577
+ const canonical = new Map<string, string>()
578
+ for (const id of candidates) {
579
+ if (byId.get(id)?.exists) canonical.set(id, (await tickets.get(id))?.ref.id ?? id)
580
+ }
581
+ const project = (ids: string[]) => [...new Set(ids.map((id) => canonical.get(id) ?? id))]
539
582
  return {
540
- blockedBy: [...new Set([...declared, ...derived])],
541
- provenance: { declared, derived },
583
+ blockedBy: project([...declared, ...derived]),
584
+ provenance: { declared: project(declared), derived: project(derived) },
542
585
  }
543
586
  }
544
587
 
@@ -1,3 +1,4 @@
1
+ import { readBuildFacts, type CurrentBuildFacts } from '../store/reduced-reads'
1
2
  import type { AbEvent } from '../events/catalog'
2
3
  import { DISPATCHER } from '../events/envelope'
3
4
  import type { EventPayload } from '../events/payloads'
@@ -10,7 +11,7 @@ export type InfrastructureCause = EventPayload<'infrastructure.failed'>['cause']
10
11
 
11
12
  export interface InfrastructureFailureInput {
12
13
  slug: string
13
- events: readonly AbEvent[]
14
+ events: readonly AbEvent[] | CurrentBuildFacts
14
15
  maxAttempts: number
15
16
  provider: string
16
17
  workspaceRef: string
@@ -23,7 +24,7 @@ export interface InfrastructureFailureInput {
23
24
  }
24
25
 
25
26
  export interface InfrastructureFailureDependencies {
26
- store: Pick<BuildStore, 'append' | 'getEvents'>
27
+ store: Pick<BuildStore, 'append' | 'getReducerValue'>
27
28
  ids: IdSource
28
29
  }
29
30
 
@@ -104,10 +105,14 @@ export async function recordInfrastructureFailure(
104
105
  deps: InfrastructureFailureDependencies,
105
106
  input: InfrastructureFailureInput,
106
107
  ): Promise<AbEvent[]> {
107
- const lastReset = infrastructureFailureResetSeq(input.events)
108
- const attempt =
109
- input.events.filter((event) => event.type === 'infrastructure.failed' && event.seq > lastReset)
110
- .length + 1
108
+ const lastReset = Array.isArray(input.events)
109
+ ? infrastructureFailureResetSeq(input.events)
110
+ : (input.events as CurrentBuildFacts).infrastructureReset
111
+ const attempt = Array.isArray(input.events)
112
+ ? input.events.filter(
113
+ (event) => event.type === 'infrastructure.failed' && event.seq > lastReset,
114
+ ).length + 1
115
+ : (input.events as CurrentBuildFacts).infrastructureCount + 1
111
116
  const message = normalizeInfrastructureError(input.error)
112
117
  const failure = await deps.store.append(input.slug, {
113
118
  actor: DISPATCHER,
@@ -130,15 +135,8 @@ export async function recordInfrastructureFailure(
130
135
 
131
136
  if (attempt < input.maxAttempts) return appended
132
137
 
133
- const currentEvents = await deps.store.getEvents(input.slug)
134
- if (
135
- !currentEvents.some(
136
- (event) =>
137
- event.seq > lastReset &&
138
- event.type === 'escalation.raised' &&
139
- event.payload.policyCause === 'infrastructure-failure-limit',
140
- )
141
- ) {
138
+ const current = (await readBuildFacts(deps.store, input.slug)).value
139
+ if (current.infrastructureRaisedSeq <= lastReset) {
142
140
  appended.push(
143
141
  await deps.store.append(input.slug, {
144
142
  actor: DISPATCHER,
@@ -1,3 +1,4 @@
1
+ import { readBuildFacts } from '../store/reduced-reads'
1
2
  /**
2
3
  * The dispatcher tick's orchestrator step (AUT-342): resume suspended turns,
3
4
  * reap crashed ones, and wake idle sessions from build attention events.
@@ -28,7 +29,6 @@ import { buildRegistry, type OperatorToolRegistry } from '../operator/registry'
28
29
  import type { OperatorSandboxService } from '../operator/sandbox'
29
30
  import { ticketBackendFromSource } from '../operator/ticket-source-backend'
30
31
  import { reduceSession, type SessionTurnTrigger } from '../store/session-reducer'
31
- import { reduceBuild } from '../kernel/reducer'
32
32
  import type { AbEvent } from '../events/catalog'
33
33
  import type { BuildStore, Clock } from '../store/types'
34
34
  import type { RepoViewStore } from './repo-view'
@@ -315,7 +315,7 @@ export async function runOrchestratorTickStep(
315
315
  event,
316
316
  buildState: (options.view !== undefined
317
317
  ? await options.view.buildState(newest.build)
318
- : reduceBuild(await store.getEvents(newest.build))) as unknown as Record<
318
+ : (await readBuildFacts(store, newest.build)).value.state) as unknown as Record<
319
319
  string,
320
320
  unknown
321
321
  >,
@@ -12,6 +12,7 @@ export const POLICY_PHASE_FAILURE_RESET_SCOPES = {
12
12
  'verify-failure-limit': 'all-phase-rounds',
13
13
  'reconcile-no-progress': 'none',
14
14
  'dispatch-failure-limit': 'none',
15
+ 'execution-no-progress': 'none',
15
16
  'setup-failure-limit': 'none',
16
17
  'infrastructure-failure-limit': 'none',
17
18
  'phase-attempt-limit': 'matching-round',
@@ -1,4 +1,3 @@
1
- import { summaryBoundary } from '../kernel/pr-summary'
2
1
  import { isRemoteWorkspace } from '../events/workspace-remote'
3
2
  /**
4
3
  * Last-chance publication settlement at the workspace-release boundary
@@ -12,11 +11,6 @@ import { isRemoteWorkspace } from '../events/workspace-remote'
12
11
  */
13
12
  import { DISPATCHER } from '../events/envelope'
14
13
  import type { AbEvent } from '../events/catalog'
15
- import {
16
- latestUncompletedPublicationRequest,
17
- publicationLostRecorded,
18
- publicationRequestCompleted,
19
- } from './publication-state'
20
14
  import type { BuildStore } from '../store/types'
21
15
 
22
16
  export interface PublicationLossDeps {
@@ -35,36 +29,56 @@ export interface PublicationLossDeps {
35
29
  export async function settlePublicationBeforeRelease(
36
30
  deps: PublicationLossDeps,
37
31
  slug: string,
38
- events: AbEvent[],
32
+ _events: AbEvent[],
39
33
  reason: string,
40
34
  ): Promise<AbEvent[]> {
41
- const request = latestUncompletedPublicationRequest(events)
35
+ const { readBuildFacts } = await import('../store/reduced-reads')
36
+ await settlePublicationFactsBeforeRelease(
37
+ deps,
38
+ slug,
39
+ (await readBuildFacts(deps.store, slug)).value,
40
+ reason,
41
+ )
42
+ // Legacy callers explicitly requested the raw event output.
43
+ return deps.store.getEvents(slug)
44
+ }
45
+
46
+ /** Current-state release guard; the returned facts follow every settlement write. */
47
+ export async function settlePublicationFactsBeforeRelease(
48
+ deps: PublicationLossDeps,
49
+ slug: string,
50
+ facts: import('../store/reduced-reads').CurrentBuildFacts,
51
+ reason: string,
52
+ ) {
53
+ const { readBuildFacts } = await import('../store/reduced-reads')
54
+ const request = facts.publication.latestUncompletedRequest()
42
55
  if (request === undefined) {
43
- const workspace = events.findLast((event) => event.type === 'workspace.provisioned')
44
56
  if (
45
- workspace?.type === 'workspace.provisioned' &&
46
- isRemoteWorkspace(workspace.payload) &&
47
- summaryBoundary(events) !== undefined
57
+ facts.workspace &&
58
+ isRemoteWorkspace(facts.workspace) &&
59
+ facts.summaryBoundary !== undefined
48
60
  ) {
49
61
  try {
50
62
  await deps.settlePublication?.(slug)
51
63
  } catch {
52
64
  /* Summary is best effort. */
53
65
  }
54
- return deps.store.getEvents(slug)
66
+ return (await readBuildFacts(deps.store, slug)).value
55
67
  }
56
- return events
68
+ return facts
57
69
  }
58
70
  try {
59
71
  await deps.settlePublication?.(slug)
60
72
  } catch {
61
- // A throw means the request is still uncompleted (or settlement failed);
62
- // either way the loss record below is what makes the release safe.
73
+ /* Record loss before release. */
63
74
  }
64
- const refreshed = await deps.store.getEvents(slug)
65
- if (publicationRequestCompleted(refreshed, request)) return refreshed
66
- if (publicationLostRecorded(refreshed, request)) return refreshed
67
- const appended = await deps.store.append(slug, {
75
+ const refreshed = (await readBuildFacts(deps.store, slug)).value
76
+ if (
77
+ refreshed.publication.requestCompleted(request) ||
78
+ refreshed.publication.lostRecorded(request)
79
+ )
80
+ return refreshed
81
+ await deps.store.append(slug, {
68
82
  actor: DISPATCHER,
69
83
  type: 'publication.lost',
70
84
  payload: {
@@ -75,5 +89,5 @@ export async function settlePublicationBeforeRelease(
75
89
  reason,
76
90
  },
77
91
  })
78
- return [...refreshed, appended]
92
+ return (await readBuildFacts(deps.store, slug)).value
79
93
  }