@defrex/autobuild 0.9.0 → 0.10.1

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 (85) hide show
  1. package/README.md +97 -113
  2. package/SPEC.md +17 -4
  3. package/docs/architecture.md +109 -0
  4. package/docs/configuration.md +26 -7
  5. package/docs/remote-store-protocol.md +35 -0
  6. package/docs/website.md +4 -0
  7. package/package.json +6 -4
  8. package/packages/core/src/cli/dashboard/facts.ts +215 -0
  9. package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
  10. package/packages/core/src/cli/dashboard/model.ts +85 -147
  11. package/packages/core/src/cli/dashboard/poll.ts +7 -2
  12. package/packages/core/src/cli/dispatch-process.ts +1 -13
  13. package/packages/core/src/cli/dispatch.ts +80 -37
  14. package/packages/core/src/cli/help.ts +15 -1
  15. package/packages/core/src/cli/init-validation.ts +18 -0
  16. package/packages/core/src/cli/init.ts +3 -2
  17. package/packages/core/src/cli/installation.ts +50 -8
  18. package/packages/core/src/cli/self-update.ts +77 -2
  19. package/packages/core/src/cli/status.ts +1 -1
  20. package/packages/core/src/cli/ticket.ts +146 -1
  21. package/packages/core/src/cli/wait.ts +10 -2
  22. package/packages/core/src/cli/watch.ts +10 -2
  23. package/packages/core/src/config/live.ts +1 -0
  24. package/packages/core/src/config/pipeline-source.ts +17 -5
  25. package/packages/core/src/config/schema.ts +21 -0
  26. package/packages/core/src/integration/harness.ts +113 -1
  27. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
  28. package/packages/core/src/kernel/auto-merge-default.ts +43 -9
  29. package/packages/core/src/kernel/auto-merge.ts +99 -22
  30. package/packages/core/src/kernel/dispatch-settings.ts +42 -17
  31. package/packages/core/src/kernel/dispatch-status.ts +23 -0
  32. package/packages/core/src/kernel/engine.ts +9 -438
  33. package/packages/core/src/kernel/generators/build-log.ts +259 -0
  34. package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
  35. package/packages/core/src/kernel/generators/session-log.ts +72 -0
  36. package/packages/core/src/kernel/harvest.ts +62 -18
  37. package/packages/core/src/kernel/incremental-contract.ts +75 -0
  38. package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
  39. package/packages/core/src/kernel/incremental.ts +53 -0
  40. package/packages/core/src/kernel/log-index.ts +620 -0
  41. package/packages/core/src/kernel/pr-attachments.ts +164 -46
  42. package/packages/core/src/kernel/reducer-registry.ts +164 -0
  43. package/packages/core/src/kernel/reducer.ts +216 -74
  44. package/packages/core/src/operator/query.ts +68 -28
  45. package/packages/core/src/operator/read-view.ts +98 -0
  46. package/packages/core/src/orchestrator/prompt.ts +3 -1
  47. package/packages/core/src/ports/tickets/fake.ts +5 -1
  48. package/packages/core/src/ports/tickets/file.ts +169 -48
  49. package/packages/core/src/ports/tickets/linear.ts +33 -1
  50. package/packages/core/src/ports/tickets/operations.ts +1 -1
  51. package/packages/core/src/ports/tickets/remote.ts +8 -1
  52. package/packages/core/src/ports/types.ts +32 -1
  53. package/packages/core/src/processes/build-execution-state.ts +31 -15
  54. package/packages/core/src/processes/build-runner.ts +184 -90
  55. package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
  56. package/packages/core/src/processes/dispatcher.ts +270 -176
  57. package/packages/core/src/processes/execution-settlement.ts +72 -39
  58. package/packages/core/src/processes/harvest-execution-state.ts +41 -29
  59. package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
  60. package/packages/core/src/processes/orchestrator-tick.ts +44 -3
  61. package/packages/core/src/processes/publication-state.ts +164 -52
  62. package/packages/core/src/processes/repo-view.ts +1436 -0
  63. package/packages/core/src/processes/sandbox-state.ts +26 -2
  64. package/packages/core/src/processes/tick-state.ts +33 -0
  65. package/packages/core/src/store/adapter.ts +10 -1
  66. package/packages/core/src/store/build-scope.ts +26 -0
  67. package/packages/core/src/store/contract.ts +337 -0
  68. package/packages/core/src/store/digest.ts +71 -18
  69. package/packages/core/src/store/local/schema.ts +17 -0
  70. package/packages/core/src/store/local/store.ts +111 -4
  71. package/packages/core/src/store/memory.ts +68 -1
  72. package/packages/core/src/store/phase-session.ts +23 -0
  73. package/packages/core/src/store/remote/client.ts +50 -1
  74. package/packages/core/src/store/remote/index.ts +2 -0
  75. package/packages/core/src/store/remote/protocol.ts +15 -0
  76. package/packages/core/src/store/repo-state-events.ts +72 -7
  77. package/packages/core/src/store/session-handle.ts +30 -0
  78. package/packages/core/src/store/session-reducer.ts +52 -11
  79. package/packages/core/src/store/snapshots.ts +88 -0
  80. package/packages/core/src/store/ticket-assets.ts +41 -8
  81. package/packages/core/src/store/types.ts +38 -0
  82. package/skills/guide/SKILL.md +38 -21
  83. package/skills/guide/references/remote-store-protocol.md +35 -0
  84. package/skills/operate/SKILL.md +146 -19
  85. package/skills/tickets/SKILL.md +57 -5
@@ -26,6 +26,7 @@
26
26
  * precedent `bulk-control.ts` set for the pause/resume walk.
27
27
  */
28
28
  import type { RepositoryEvent } from '../events/repository'
29
+ import { defineReducer } from './incremental'
29
30
  import { discardInFlight, type BuildState } from './reducer'
30
31
 
31
32
  /** One `dispatcher.auto-merge-default-set` fact, resolved. `actor` is the
@@ -49,16 +50,49 @@ export function latestAutoMergeDefault(
49
50
  events: RepositoryEvent[],
50
51
  enabled?: boolean,
51
52
  ): AutoMergeDefaultFact | undefined {
52
- for (let index = events.length - 1; index >= 0; index -= 1) {
53
- const event = events[index]
54
- if (
55
- event?.type === 'dispatcher.auto-merge-default-set' &&
56
- (enabled === undefined || event.payload.enabled === enabled)
57
- ) {
58
- return { enabled: event.payload.enabled, seq: event.seq, actor: event.actor }
53
+ return pickAutoMergeDefault(autoMergeDefaultReducer.reduce(events), enabled)
54
+ }
55
+
56
+ export const AUTO_MERGE_DEFAULT_REDUCER_VERSION = 1
57
+
58
+ /** The newest `dispatcher.auto-merge-default-set` fact in array order, overall
59
+ * and per value — `latestAutoMergeDefault(events, enabled)` is a lookup in
60
+ * this state, exact for any array order. */
61
+ export interface AutoMergeDefaultFacts {
62
+ latest?: AutoMergeDefaultFact
63
+ latestEnabled?: AutoMergeDefaultFact
64
+ latestDisabled?: AutoMergeDefaultFact
65
+ }
66
+
67
+ export const autoMergeDefaultReducer = defineReducer<
68
+ AutoMergeDefaultFacts,
69
+ RepositoryEvent,
70
+ AutoMergeDefaultFacts
71
+ >({
72
+ version: AUTO_MERGE_DEFAULT_REDUCER_VERSION,
73
+ initial: () => ({}),
74
+ fold(acc, events) {
75
+ for (const event of events) {
76
+ if (event.type !== 'dispatcher.auto-merge-default-set') continue
77
+ const fact: AutoMergeDefaultFact = {
78
+ enabled: event.payload.enabled,
79
+ seq: event.seq,
80
+ actor: event.actor,
81
+ }
82
+ acc.latest = fact
83
+ if (fact.enabled) acc.latestEnabled = fact
84
+ else acc.latestDisabled = fact
59
85
  }
60
- }
61
- return undefined
86
+ },
87
+ finish: (acc) => ({ ...acc }),
88
+ })
89
+
90
+ export function pickAutoMergeDefault(
91
+ facts: AutoMergeDefaultFacts,
92
+ enabled?: boolean,
93
+ ): AutoMergeDefaultFact | undefined {
94
+ if (enabled === undefined) return facts.latest
95
+ return enabled ? facts.latestEnabled : facts.latestDisabled
62
96
  }
63
97
 
64
98
  /**
@@ -2,6 +2,7 @@ import type { AbEvent, EventEnvelope, EventWrite } from '../events/catalog'
2
2
  import { KERNEL } from '../events/envelope'
3
3
  import { autoMergeDeferralClasses, type AutoMergeDeferralReason } from '../ports/types'
4
4
  import type { BuildStore } from '../store/types'
5
+ import { defineReducer } from './incremental'
5
6
  import type { BuildState } from './reducer'
6
7
 
7
8
  /** The complete GitHub `mergeStateStatus` enum. Keeping this list closed is a
@@ -133,36 +134,104 @@ export function currentDeferralObservation(
133
134
  events: AbEvent[],
134
135
  prNumber: number,
135
136
  commandSeq: number,
136
- ): Extract<AbEvent, { type: 'observation.recorded' }> | undefined {
137
+ ): DeferralObservation | undefined {
138
+ return deferralObservationFrom(
139
+ currentDeferralObservationReducer.reduce(events),
140
+ prNumber,
141
+ commandSeq,
142
+ )
143
+ }
144
+
145
+ type DeferralObservation = Extract<AbEvent, { type: 'observation.recorded' }>
146
+
147
+ const DEFERRAL_REF_PREFIX = 'auto-merge-gate:pr:'
148
+
149
+ /** One array-order ledger entry. Only the facts the currency predicate reads
150
+ * are kept, with every maximal run of other events collapsed to a barrier
151
+ * carrying the run's max seq: the head scan stops at the first event (of ANY
152
+ * type) with `seq >= observation.seq`, so an unrelated event must still be able
153
+ * to end it. */
154
+ type DeferralLedgerEntry =
155
+ | { t: 'barrier'; seq: number }
156
+ | { t: 'observation'; event: DeferralObservation }
157
+ | { t: 'finalized'; seq: number; headSha: string }
158
+ | { t: 'terminal'; seq: number }
159
+ | { t: 'reconciled'; seq: number }
160
+
161
+ export const CURRENT_DEFERRAL_OBSERVATION_REDUCER_VERSION = 1
162
+
163
+ export interface DeferralLedger {
164
+ entries: DeferralLedgerEntry[]
165
+ }
166
+
167
+ export const currentDeferralObservationReducer = defineReducer<
168
+ DeferralLedger,
169
+ AbEvent,
170
+ DeferralLedger
171
+ >({
172
+ version: CURRENT_DEFERRAL_OBSERVATION_REDUCER_VERSION,
173
+ initial: () => ({ entries: [] }),
174
+ fold(acc, events) {
175
+ for (const event of events) {
176
+ let entry: DeferralLedgerEntry | undefined
177
+ if (
178
+ event.type === 'observation.recorded' &&
179
+ event.payload.refs?.some((ref) => ref.startsWith(DEFERRAL_REF_PREFIX)) === true
180
+ ) {
181
+ entry = { t: 'observation', event }
182
+ } else if (event.type === 'finalize.completed') {
183
+ entry = { t: 'finalized', seq: event.seq, headSha: event.payload.pr.headSha }
184
+ } else if (event.type === 'pr.merged' || event.type === 'pr.closed') {
185
+ entry = { t: 'terminal', seq: event.seq }
186
+ } else if (event.type === 'reconcile.completed') {
187
+ entry = { t: 'reconciled', seq: event.seq }
188
+ }
189
+ if (entry !== undefined) {
190
+ acc.entries.push(entry)
191
+ continue
192
+ }
193
+ const last = acc.entries.at(-1)
194
+ if (last?.t === 'barrier') last.seq = Math.max(last.seq, event.seq)
195
+ else acc.entries.push({ t: 'barrier', seq: event.seq })
196
+ }
197
+ },
198
+ finish: (acc) => ({ entries: acc.entries.slice() }),
199
+ })
200
+
201
+ function deferralEntrySeq(entry: DeferralLedgerEntry): number {
202
+ return entry.t === 'observation' ? entry.event.seq : entry.seq
203
+ }
204
+
205
+ /** The current deferral observation for a PR/command over a reduced ledger. */
206
+ export function deferralObservationFrom(
207
+ ledger: DeferralLedger,
208
+ prNumber: number,
209
+ commandSeq: number,
210
+ ): DeferralObservation | undefined {
137
211
  const marker = autoMergeDeferralRef(prNumber, commandSeq)
138
- let observation: Extract<AbEvent, { type: 'observation.recorded' }> | undefined
139
- for (let index = events.length - 1; index >= 0; index -= 1) {
140
- const event = events[index]
141
- if (
142
- event !== undefined &&
143
- event.type === 'observation.recorded' &&
144
- event.payload.refs?.includes(marker) === true
145
- ) {
146
- observation = event
212
+ const { entries } = ledger
213
+ let observation: DeferralObservation | undefined
214
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
215
+ const entry = entries[index]
216
+ if (entry?.t === 'observation' && entry.event.payload.refs?.includes(marker) === true) {
217
+ observation = entry.event
147
218
  break
148
219
  }
149
220
  }
150
221
  if (observation === undefined) return undefined
151
222
 
152
223
  let headAtObservation: string | undefined
153
- for (const event of events) {
154
- if (event.seq >= observation.seq) break
155
- if (event.type === 'finalize.completed') headAtObservation = event.payload.pr.headSha
224
+ for (const entry of entries) {
225
+ if (deferralEntrySeq(entry) >= observation.seq) break
226
+ if (entry.t === 'finalized') headAtObservation = entry.headSha
156
227
  }
157
228
 
158
- for (const event of events) {
159
- if (event.seq <= observation.seq) continue
160
- if (event.type === 'pr.merged' || event.type === 'pr.closed') return undefined
161
- if (event.type === 'reconcile.completed') return undefined
162
- if (event.type === 'finalize.completed') {
163
- if (headAtObservation === undefined) headAtObservation = event.payload.pr.headSha
164
- else if (event.payload.pr.headSha !== headAtObservation) return undefined
165
- }
229
+ for (const entry of entries) {
230
+ if (entry.t === 'observation' || entry.t === 'barrier') continue
231
+ if (entry.seq <= observation.seq) continue
232
+ if (entry.t === 'terminal' || entry.t === 'reconciled') return undefined
233
+ if (headAtObservation === undefined) headAtObservation = entry.headSha
234
+ else if (entry.headSha !== headAtObservation) return undefined
166
235
  }
167
236
  return observation
168
237
  }
@@ -186,10 +255,18 @@ export function hasAutoMergeDeferralObservation(
186
255
  export function currentAutoMergeDeferral(
187
256
  events: AbEvent[],
188
257
  state: Pick<BuildState, 'autoMerge' | 'pr'>,
258
+ ): string | undefined {
259
+ return currentAutoMergeDeferralFrom(currentDeferralObservationReducer.reduce(events), state)
260
+ }
261
+
262
+ /** `currentAutoMergeDeferral` over an already-reduced ledger. */
263
+ export function currentAutoMergeDeferralFrom(
264
+ ledger: DeferralLedger,
265
+ state: Pick<BuildState, 'autoMerge' | 'pr'>,
189
266
  ): string | undefined {
190
267
  const pending = pendingAutoMerge(state)
191
268
  if (pending?.enabled !== true || state.pr === undefined) return undefined
192
- return currentDeferralObservation(events, state.pr.number, pending.commandSeq)?.payload.summary
269
+ return deferralObservationFrom(ledger, state.pr.number, pending.commandSeq)?.payload.summary
193
270
  }
194
271
 
195
272
  const DEFERRAL_SUMMARIES = {
@@ -4,6 +4,7 @@
4
4
  * is authoritative; fresh repositories retain the historical process defaults.
5
5
  */
6
6
  import type { RepositoryEvent } from '../events/repository'
7
+ import { defineReducer, type IncrementalReducer } from './incremental'
7
8
 
8
9
  export const DEFAULT_DISPATCH_INTAKE = true
9
10
  export const DEFAULT_DISPATCH_PAUSED = false
@@ -21,32 +22,36 @@ export interface DispatchSettings {
21
22
  defaultAutoMerge: boolean
22
23
  }
23
24
 
24
- export function reduceDispatchSettings(events: RepositoryEvent[]): DispatchSettings {
25
- let intake = DEFAULT_DISPATCH_INTAKE
26
- let paused = DEFAULT_DISPATCH_PAUSED
27
- let defaultAutoMerge = DEFAULT_DISPATCH_AUTO_MERGE
28
- let intakeSeq = 0
29
- let pausedSeq = 0
30
- let autoMergeSeq = 0
25
+ /** The carried accumulator: the settings plus the seq of the fact that set
26
+ * each, which decides last-writer-wins independent of array order. */
27
+ export interface DispatchSettingsAcc extends DispatchSettings {
28
+ intakeSeq: number
29
+ pausedSeq: number
30
+ autoMergeSeq: number
31
+ }
32
+
33
+ /** Bump when `DispatchSettingsAcc`'s shape or fold semantics change. */
34
+ export const DISPATCH_SETTINGS_REDUCER_VERSION = 1
31
35
 
36
+ function foldDispatchSettings(acc: DispatchSettingsAcc, events: readonly RepositoryEvent[]): void {
32
37
  for (const event of events) {
33
38
  switch (event.type) {
34
39
  case 'dispatcher.intake-set':
35
- if (event.seq > intakeSeq) {
36
- intake = event.payload.enabled
37
- intakeSeq = event.seq
40
+ if (event.seq > acc.intakeSeq) {
41
+ acc.intake = event.payload.enabled
42
+ acc.intakeSeq = event.seq
38
43
  }
39
44
  break
40
45
  case 'dispatcher.pause-set':
41
- if (event.seq > pausedSeq) {
42
- paused = event.payload.enabled
43
- pausedSeq = event.seq
46
+ if (event.seq > acc.pausedSeq) {
47
+ acc.paused = event.payload.enabled
48
+ acc.pausedSeq = event.seq
44
49
  }
45
50
  break
46
51
  case 'dispatcher.auto-merge-default-set':
47
- if (event.seq > autoMergeSeq) {
48
- defaultAutoMerge = event.payload.enabled
49
- autoMergeSeq = event.seq
52
+ if (event.seq > acc.autoMergeSeq) {
53
+ acc.defaultAutoMerge = event.payload.enabled
54
+ acc.autoMergeSeq = event.seq
50
55
  }
51
56
  break
52
57
  default:
@@ -55,6 +60,26 @@ export function reduceDispatchSettings(events: RepositoryEvent[]): DispatchSetti
55
60
  break
56
61
  }
57
62
  }
63
+ }
64
+
65
+ export const dispatchSettingsReducer: IncrementalReducer<
66
+ DispatchSettingsAcc,
67
+ RepositoryEvent,
68
+ DispatchSettings
69
+ > = defineReducer({
70
+ version: DISPATCH_SETTINGS_REDUCER_VERSION,
71
+ initial: () => ({
72
+ intake: DEFAULT_DISPATCH_INTAKE,
73
+ paused: DEFAULT_DISPATCH_PAUSED,
74
+ defaultAutoMerge: DEFAULT_DISPATCH_AUTO_MERGE,
75
+ intakeSeq: 0,
76
+ pausedSeq: 0,
77
+ autoMergeSeq: 0,
78
+ }),
79
+ fold: foldDispatchSettings,
80
+ finish: ({ intake, paused, defaultAutoMerge }) => ({ intake, paused, defaultAutoMerge }),
81
+ })
58
82
 
59
- return { intake, paused, defaultAutoMerge }
83
+ export function reduceDispatchSettings(events: RepositoryEvent[]): DispatchSettings {
84
+ return dispatchSettingsReducer.reduce(events)
60
85
  }
@@ -1,5 +1,6 @@
1
1
  import type { RepositoryEvent } from '../events/repository'
2
2
  import type { ArtifactRef } from '../ontology'
3
+ import type { IncrementalReducer } from './incremental'
3
4
 
4
5
  export type DispatchHealth = 'starting' | 'running' | 'stopped' | 'failed'
5
6
 
@@ -139,3 +140,25 @@ export function reduceDispatchStatus(
139
140
  }
140
141
  return state
141
142
  }
143
+
144
+ /** Bump when `DispatchStatus`'s shape or fold semantics change (it is its own
145
+ * accumulator: `lastSeq` makes the fold skip already-seen events). */
146
+ export const DISPATCH_STATUS_REDUCER_VERSION = 1
147
+
148
+ /** The incremental form of `reduceDispatchStatus` for one run. The run is a
149
+ * parameter, so this is a factory. */
150
+ export function dispatchStatusReducer(
151
+ run: string,
152
+ ): IncrementalReducer<DispatchStatus, RepositoryEvent, DispatchStatus> {
153
+ return {
154
+ version: DISPATCH_STATUS_REDUCER_VERSION,
155
+ initial: () => ({ run, health: 'starting', roleWarnings: [], diagnostics: [], lastSeq: 0 }),
156
+ advance: (acc, events) => reduceDispatchStatus(events, run, acc),
157
+ finish: (acc) => ({
158
+ ...acc,
159
+ roleWarnings: [...acc.roleWarnings],
160
+ diagnostics: [...acc.diagnostics],
161
+ }),
162
+ reduce: (events) => reduceDispatchStatus(events, run),
163
+ }
164
+ }