@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.
- package/README.md +1 -1
- package/SPEC.md +138 -16
- package/bin/ab-dispatch-kernel.ts +4 -6
- package/docs/architecture.md +155 -73
- package/docs/configuration.md +41 -9
- package/docs/hosted-service.md +31 -5
- package/docs/operator-api.md +8 -1
- package/docs/remote-store-protocol.md +94 -2
- package/docs/remote-ticket-protocol.md +151 -9
- package/docs/setup.md +33 -7
- package/docs/ticket-migration.md +64 -0
- package/package.json +4 -3
- package/packages/core/src/cli/attention.ts +18 -164
- package/packages/core/src/cli/build-control.ts +23 -16
- package/packages/core/src/cli/bulk-control.ts +3 -2
- package/packages/core/src/cli/connect.ts +141 -38
- package/packages/core/src/cli/context.ts +23 -79
- package/packages/core/src/cli/dashboard/model.ts +36 -22
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch-frontend.ts +24 -67
- package/packages/core/src/cli/dispatch-process.ts +14 -7
- package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
- package/packages/core/src/cli/dispatch.ts +304 -82
- package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
- package/packages/core/src/cli/harvest.ts +84 -42
- package/packages/core/src/cli/help.ts +14 -8
- package/packages/core/src/cli/historical-build-state.ts +27 -0
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +23 -5
- package/packages/core/src/cli/mcp.ts +2 -1
- package/packages/core/src/cli/pr-attachments.ts +10 -13
- package/packages/core/src/cli/pr-summary-publication.ts +27 -60
- package/packages/core/src/cli/pr-summary.ts +43 -13
- package/packages/core/src/cli/publication-settlement.ts +10 -20
- package/packages/core/src/cli/repository-status.ts +38 -2
- package/packages/core/src/cli/self-update.ts +5 -1
- package/packages/core/src/cli/status.ts +62 -32
- package/packages/core/src/cli/terminals.ts +25 -142
- package/packages/core/src/cli/ticket-assets.ts +7 -2
- package/packages/core/src/cli/ticket-editor.ts +82 -0
- package/packages/core/src/cli/ticket.ts +166 -36
- package/packages/core/src/cli/wait.ts +91 -84
- package/packages/core/src/cli/watch.ts +60 -97
- package/packages/core/src/config/pipeline-source.ts +4 -3
- package/packages/core/src/config/schema.ts +35 -3
- package/packages/core/src/connection.ts +3 -3
- package/packages/core/src/events/catalog.ts +3 -2
- package/packages/core/src/events/payloads.ts +6 -0
- package/packages/core/src/events/repository.ts +7 -3
- package/packages/core/src/integration/harness.ts +38 -4
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2444 -9
- package/packages/core/src/kernel/auto-merge.ts +19 -3
- package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
- package/packages/core/src/kernel/generators/build-log.ts +1 -1
- package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
- package/packages/core/src/kernel/harvest.ts +25 -23
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- package/packages/core/src/kernel/log-index-value.ts +38 -0
- package/packages/core/src/kernel/read-facts.ts +637 -0
- package/packages/core/src/kernel/reducer-registry.ts +139 -3
- package/packages/core/src/kernel/repository-attention.ts +179 -0
- package/packages/core/src/kernel/service-facts.ts +135 -0
- package/packages/core/src/kernel/terminal-guard.ts +133 -0
- package/packages/core/src/ontology.ts +1 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/control.ts +5 -6
- package/packages/core/src/operator/index.ts +7 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +37 -20
- package/packages/core/src/operator/read-view.ts +4 -13
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/requests.ts +2 -14
- package/packages/core/src/operator/sandbox.ts +10 -9
- package/packages/core/src/operator/ticket-source-backend.ts +17 -21
- package/packages/core/src/operator/tickets.ts +132 -22
- package/packages/core/src/plugin-sdk/index.ts +19 -0
- package/packages/core/src/plugins/runtimes.ts +5 -1
- package/packages/core/src/ports/runner/session-env.ts +2 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +48 -12
- package/packages/core/src/ports/tickets/fake.ts +30 -1
- package/packages/core/src/ports/tickets/file-assets.ts +159 -0
- package/packages/core/src/ports/tickets/file-codec.ts +90 -0
- package/packages/core/src/ports/tickets/file.ts +114 -120
- package/packages/core/src/ports/tickets/grants.ts +47 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +17 -0
- package/packages/core/src/ports/tickets/identity.ts +20 -0
- package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
- package/packages/core/src/ports/tickets/linear.ts +74 -9
- package/packages/core/src/ports/tickets/migration.ts +336 -0
- package/packages/core/src/ports/tickets/operations.ts +38 -7
- package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +95 -0
- package/packages/core/src/ports/tickets/remote.ts +60 -3
- package/packages/core/src/ports/tickets/requested-id.ts +46 -0
- package/packages/core/src/ports/tickets/revision.ts +62 -0
- package/packages/core/src/ports/tickets/states.ts +61 -0
- package/packages/core/src/ports/types.ts +40 -3
- package/packages/core/src/ports/workspace/build-execution.ts +1 -0
- package/packages/core/src/ports/workspace/create.ts +5 -1
- package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +8 -4
- package/packages/core/src/processes/build-child.ts +6 -6
- package/packages/core/src/processes/build-runner.ts +62 -70
- package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
- package/packages/core/src/processes/dispatcher.ts +223 -205
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +30 -4
- package/packages/core/src/processes/harvest-execution-state.ts +15 -9
- package/packages/core/src/processes/harvest-runner.ts +58 -74
- package/packages/core/src/processes/harvest.ts +72 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
- package/packages/core/src/processes/orchestrator-tick.ts +2 -2
- package/packages/core/src/processes/phase-failure-budget.ts +1 -0
- package/packages/core/src/processes/publication-loss.ts +35 -21
- package/packages/core/src/processes/repo-view.ts +229 -1099
- package/packages/core/src/processes/tick-publication.ts +110 -0
- package/packages/core/src/store/adapter.ts +23 -0
- package/packages/core/src/store/build-scope.ts +17 -0
- package/packages/core/src/store/contract.ts +283 -1
- package/packages/core/src/store/local/schema.ts +1 -0
- package/packages/core/src/store/local/store.ts +180 -55
- package/packages/core/src/store/memory.ts +119 -29
- package/packages/core/src/store/phase-session.ts +17 -0
- package/packages/core/src/store/reduced-reads.ts +239 -0
- package/packages/core/src/store/reducer-value-shapes.json +14056 -0
- package/packages/core/src/store/reducer-value-validation.ts +96 -0
- package/packages/core/src/store/reducer-values.ts +145 -0
- package/packages/core/src/store/remote/client.ts +41 -0
- package/packages/core/src/store/remote/compatibility.ts +2 -0
- package/packages/core/src/store/remote/index.ts +19 -0
- package/packages/core/src/store/remote/protocol.ts +16 -0
- package/packages/core/src/store/remote/version.ts +3 -1
- package/packages/core/src/store/repo-state-events.ts +1 -1
- package/packages/core/src/store/repo-tick.ts +72 -0
- package/packages/core/src/store/session-handle.ts +19 -0
- package/packages/core/src/store/snapshots.ts +10 -0
- package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
- package/packages/core/src/store/ticket-assets.ts +86 -8
- package/packages/core/src/store/types.ts +40 -3
- package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
- package/packages/core/src/testing/index.ts +8 -0
- package/packages/core/src/testing/reduced-read-budget.ts +462 -0
- package/skills/guide/SKILL.md +97 -12
- package/skills/guide/references/operator-api.md +8 -1
- package/skills/guide/references/remote-store-protocol.md +94 -2
- package/skills/guide/references/remote-ticket-protocol.md +151 -9
- package/skills/guide/references/setup.md +33 -7
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +2 -2
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { parseConfig } from '../config/load'
|
|
2
|
+
import { HostedTicketSource } from '../ports/tickets/remote'
|
|
3
|
+
import { ticketLifecycle } from '../ports/tickets/lifecycle'
|
|
1
4
|
import { createHash, randomBytes, randomUUID } from 'node:crypto'
|
|
2
5
|
import { access, readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
3
6
|
import { constants } from 'node:fs'
|
|
@@ -14,6 +17,8 @@ import { resolveMainRepo } from './repo-state'
|
|
|
14
17
|
import { ProjectCredentials } from './project-credentials'
|
|
15
18
|
|
|
16
19
|
interface ConnectOptions {
|
|
20
|
+
tickets?: boolean
|
|
21
|
+
builds?: boolean
|
|
17
22
|
cwd: string
|
|
18
23
|
env: Record<string, string | undefined>
|
|
19
24
|
exec: Exec
|
|
@@ -45,8 +50,15 @@ async function project(opts: ConnectOptions) {
|
|
|
45
50
|
const store = table.store as { url?: unknown } | undefined
|
|
46
51
|
if (store && (typeof store !== 'object' || Array.isArray(store)))
|
|
47
52
|
throw new Error('Invalid [store] configuration')
|
|
48
|
-
const
|
|
49
|
-
|
|
53
|
+
const tickets = table.tickets as { source?: string; url?: string } | undefined
|
|
54
|
+
const buildService = opts.env.AB_STORE?.trim() || store?.url
|
|
55
|
+
const ticketService = tickets?.source === 'hosted' ? (tickets.url ?? buildService) : undefined
|
|
56
|
+
const selected = opts.tickets ? ticketService : buildService
|
|
57
|
+
const service =
|
|
58
|
+
typeof selected === 'string' && /^https?:/.test(selected)
|
|
59
|
+
? normalizeServiceURL(selected)
|
|
60
|
+
: undefined
|
|
61
|
+
return { checkout, repo, path, text, table, service, buildService, ticketService }
|
|
50
62
|
}
|
|
51
63
|
async function request(
|
|
52
64
|
opts: ConnectOptions,
|
|
@@ -84,45 +96,64 @@ async function atomicConfig(path: string, before: string, after: string) {
|
|
|
84
96
|
}
|
|
85
97
|
}
|
|
86
98
|
export async function abConnect(raw: string | undefined, opts: ConnectOptions): Promise<void> {
|
|
99
|
+
if (opts.tickets && opts.builds) throw new Error('Choose --tickets or --builds')
|
|
87
100
|
const p = await project(opts)
|
|
101
|
+
if (opts.tickets && (p.table.tickets as { source?: string } | undefined)?.source !== 'hosted')
|
|
102
|
+
throw new Error('ab connect --tickets requires [tickets] source = "hosted"')
|
|
88
103
|
const credentials = new ProjectCredentials(opts.env)
|
|
89
104
|
if (raw === undefined) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
const connections = new Map<string, string[]>()
|
|
106
|
+
for (const [use, candidate] of [
|
|
107
|
+
['builds', p.buildService],
|
|
108
|
+
['tickets', p.ticketService],
|
|
109
|
+
] as const) {
|
|
110
|
+
if (typeof candidate !== 'string' || !/^https?:/.test(candidate)) continue
|
|
111
|
+
const service = normalizeServiceURL(candidate)
|
|
112
|
+
connections.set(service, [...(connections.get(service) ?? []), use])
|
|
113
|
+
}
|
|
114
|
+
if (connections.size === 0) {
|
|
115
|
+
opts.print(`Project: ${p.repo}\nService: unconfigured`)
|
|
94
116
|
return
|
|
95
117
|
}
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
118
|
+
for (const [service, uses] of connections) {
|
|
119
|
+
opts.print(`Uses: ${uses.join(', ')}`)
|
|
120
|
+
if (!service) {
|
|
121
|
+
opts.print(
|
|
122
|
+
`Project: ${p.repo}\nService: unconfigured\nCredential: missing; run ab connect <url>`,
|
|
123
|
+
)
|
|
124
|
+
return
|
|
125
|
+
}
|
|
126
|
+
const entry = credentials.load(service, p.repo)
|
|
127
|
+
let status = 'missing; run ab connect'
|
|
128
|
+
if (entry) {
|
|
129
|
+
try {
|
|
130
|
+
const response = await request(opts, service, 'status', undefined, entry.token)
|
|
131
|
+
if (response.ok) {
|
|
132
|
+
const body = (await response.json()) as {
|
|
133
|
+
repo?: string
|
|
134
|
+
credentialId?: string
|
|
135
|
+
accepted?: boolean
|
|
136
|
+
}
|
|
137
|
+
if (body.repo !== p.repo || body.credentialId !== entry.id || body.accepted !== true)
|
|
138
|
+
throw new Error('Invalid status response')
|
|
106
139
|
}
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
status = response.ok
|
|
141
|
+
? 'accepted'
|
|
142
|
+
: response.status === 401 || response.status === 403
|
|
143
|
+
? 'rejected; run ab connect'
|
|
144
|
+
: 'unreachable'
|
|
145
|
+
} catch {
|
|
146
|
+
status = 'unreachable'
|
|
109
147
|
}
|
|
110
|
-
status = response.ok
|
|
111
|
-
? 'accepted'
|
|
112
|
-
: response.status === 401 || response.status === 403
|
|
113
|
-
? 'rejected; run ab connect'
|
|
114
|
-
: 'unreachable'
|
|
115
|
-
} catch {
|
|
116
|
-
status = 'unreachable'
|
|
117
148
|
}
|
|
149
|
+
opts.print(`Service: ${service}\nProject: ${p.repo}\nCredential: ${status}`)
|
|
118
150
|
}
|
|
119
|
-
opts.print(`Service: ${p.service}\nProject: ${p.repo}\nCredential: ${status}`)
|
|
120
151
|
return
|
|
121
152
|
}
|
|
122
153
|
const service = normalizeServiceURL(raw)
|
|
123
|
-
if (p.service && p.service !== service)
|
|
154
|
+
if (!opts.tickets && p.service && p.service !== service)
|
|
124
155
|
throw new Error(
|
|
125
|
-
|
|
156
|
+
`Configured [${opts.tickets ? 'tickets' : 'store'}] url differs; update it explicitly before connecting to another service`,
|
|
126
157
|
)
|
|
127
158
|
await access(p.path, constants.W_OK)
|
|
128
159
|
await access(dirname(p.path), constants.W_OK)
|
|
@@ -169,6 +200,7 @@ export async function abConnect(raw: string | undefined, opts: ConnectOptions):
|
|
|
169
200
|
let approved = false
|
|
170
201
|
let configChanged = false
|
|
171
202
|
let saved = false
|
|
203
|
+
let updatedText = p.text
|
|
172
204
|
try {
|
|
173
205
|
let outcome: ConnectionOutcome | undefined
|
|
174
206
|
while (!opts.signal?.aborted && now() <= Date.parse(started.expiresAt) + interval) {
|
|
@@ -199,15 +231,42 @@ export async function abConnect(raw: string | undefined, opts: ConnectOptions):
|
|
|
199
231
|
approved = true
|
|
200
232
|
if (outcome.repo !== p.repo || outcome.credentialId !== id)
|
|
201
233
|
throw new Error('Approval identity mismatch')
|
|
202
|
-
if (!p.service) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
throw new Error('Cannot safely update [store] url; add it manually and retry')
|
|
208
|
-
await atomicConfig(p.path, p.text, after)
|
|
234
|
+
if (!p.service || (opts.tickets && (p.table.tickets as { url?: string }).url !== service)) {
|
|
235
|
+
updatedText = setConnectionURL(p.text, opts.tickets ? 'tickets' : 'store', service)
|
|
236
|
+
if (opts.tickets) parseConfig(updatedText)
|
|
237
|
+
else parse(updatedText)
|
|
238
|
+
await atomicConfig(p.path, p.text, updatedText)
|
|
209
239
|
configChanged = true
|
|
210
240
|
}
|
|
241
|
+
const configuredTickets = p.table.tickets as { source?: string; url?: string } | undefined
|
|
242
|
+
if (
|
|
243
|
+
opts.tickets ||
|
|
244
|
+
(configuredTickets?.source === 'hosted' &&
|
|
245
|
+
(!configuredTickets.url || normalizeServiceURL(configuredTickets.url) === service))
|
|
246
|
+
) {
|
|
247
|
+
const tickets = parseConfig(updatedText).tickets
|
|
248
|
+
const lifecycle = ticketLifecycle(tickets)
|
|
249
|
+
await new HostedTicketSource({
|
|
250
|
+
url: service,
|
|
251
|
+
token,
|
|
252
|
+
repo: p.repo,
|
|
253
|
+
teamKey: tickets.teamKey!,
|
|
254
|
+
...(opts.fetch ? { fetchFn: (input, init) => opts.fetch!(String(input), init) } : {}),
|
|
255
|
+
}).synchronizePolicy({
|
|
256
|
+
version: 1,
|
|
257
|
+
tickets: {
|
|
258
|
+
source: 'hosted',
|
|
259
|
+
teamKey: tickets.teamKey!,
|
|
260
|
+
readyState: lifecycle.ready,
|
|
261
|
+
triageState: lifecycle.triage,
|
|
262
|
+
claimedState: lifecycle.doing,
|
|
263
|
+
doneState: lifecycle.done,
|
|
264
|
+
createState: lifecycle.create,
|
|
265
|
+
proposalState: lifecycle.proposal,
|
|
266
|
+
...(tickets.readyLabels !== undefined ? { readyLabels: tickets.readyLabels } : {}),
|
|
267
|
+
},
|
|
268
|
+
})
|
|
269
|
+
}
|
|
211
270
|
credentials.save({ service, repo: p.repo, id, token })
|
|
212
271
|
saved = true
|
|
213
272
|
if (previous) {
|
|
@@ -233,21 +292,35 @@ export async function abConnect(raw: string | undefined, opts: ConnectOptions):
|
|
|
233
292
|
}
|
|
234
293
|
if (configChanged) {
|
|
235
294
|
try {
|
|
236
|
-
await atomicConfig(p.path,
|
|
295
|
+
await atomicConfig(p.path, updatedText, p.text)
|
|
237
296
|
} catch {
|
|
238
|
-
opts.print(
|
|
297
|
+
opts.print(
|
|
298
|
+
`Restore [${opts.tickets ? 'tickets' : 'store'}] url manually if needed; configuration changed concurrently.`,
|
|
299
|
+
)
|
|
239
300
|
}
|
|
240
301
|
}
|
|
241
302
|
}
|
|
242
303
|
throw new Error(
|
|
243
304
|
approved
|
|
244
|
-
?
|
|
305
|
+
? `Connection could not be saved; previous credential retained. ${(error as Error).message}. Repair the reported error and run ab connect again.`
|
|
245
306
|
: (error as Error).message,
|
|
246
307
|
)
|
|
247
308
|
}
|
|
248
309
|
}
|
|
249
310
|
export async function abDisconnect(opts: ConnectOptions): Promise<void> {
|
|
311
|
+
if (opts.tickets && opts.builds) throw new Error('Choose --tickets or --builds')
|
|
250
312
|
const p = await project(opts)
|
|
313
|
+
if (
|
|
314
|
+
!opts.tickets &&
|
|
315
|
+
!opts.builds &&
|
|
316
|
+
p.ticketService &&
|
|
317
|
+
p.service &&
|
|
318
|
+
normalizeServiceURL(String(p.ticketService)) !== p.service
|
|
319
|
+
)
|
|
320
|
+
throw new Error(
|
|
321
|
+
'Multiple services: use ab disconnect --tickets for tickets; set AB_STORE to the build service and select --builds for builds',
|
|
322
|
+
)
|
|
323
|
+
if (!p.service && !opts.builds && p.ticketService) return abDisconnect({ ...opts, tickets: true })
|
|
251
324
|
if (!p.service) throw new Error('No configured service')
|
|
252
325
|
const credentials = new ProjectCredentials(opts.env)
|
|
253
326
|
const entry = credentials.load(p.service, p.repo)
|
|
@@ -255,7 +328,37 @@ export async function abDisconnect(opts: ConnectOptions): Promise<void> {
|
|
|
255
328
|
opts.print('No stored credential; project configuration and service data retained.')
|
|
256
329
|
return
|
|
257
330
|
}
|
|
331
|
+
if (p.ticketService && p.buildService === p.ticketService)
|
|
332
|
+
opts.print('Builds and tickets share this credential; disconnecting affects both uses.')
|
|
258
333
|
await revoke(opts, p.service, entry.token)
|
|
259
334
|
credentials.remove(p.service, p.repo)
|
|
260
335
|
opts.print('Disconnected; project registration, data, and [store] url retained.')
|
|
261
336
|
}
|
|
337
|
+
|
|
338
|
+
/** Edit only the selected table; preserve unrelated configuration and comments. */
|
|
339
|
+
export function setConnectionURL(
|
|
340
|
+
text: string,
|
|
341
|
+
table: 'tickets' | 'store',
|
|
342
|
+
service: string,
|
|
343
|
+
): string {
|
|
344
|
+
const lines = text.split('\n')
|
|
345
|
+
let inside = false
|
|
346
|
+
let start = -1
|
|
347
|
+
for (let i = 0; i < lines.length; i++) {
|
|
348
|
+
const line = lines[i]!
|
|
349
|
+
if (/^\s*\[/.test(line)) {
|
|
350
|
+
if (inside) {
|
|
351
|
+
lines.splice(i, 0, `url = ${JSON.stringify(service)}`)
|
|
352
|
+
return lines.join('\n')
|
|
353
|
+
}
|
|
354
|
+
inside = new RegExp(`^\\s*\\[${table}\\]\\s*(?:#.*)?$`).test(line)
|
|
355
|
+
if (inside) start = i
|
|
356
|
+
} else if (inside && /^\s*url\s*=/.test(line)) {
|
|
357
|
+
lines[i] = `url = ${JSON.stringify(service)}`
|
|
358
|
+
return lines.join('\n')
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
if (start >= 0) lines.splice(start + 1, 0, `url = ${JSON.stringify(service)}`)
|
|
362
|
+
else lines.push(`[${table}]`, `url = ${JSON.stringify(service)}`)
|
|
363
|
+
return lines.join('\n')
|
|
364
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readBuildFacts, type CurrentBuildFacts } from '../store/reduced-reads'
|
|
1
2
|
/**
|
|
2
3
|
* The `ab context` materializer (SPEC §8.3): a phase-scoped hydration of the
|
|
3
4
|
* gitignored `.ab/` scratch dir. Everything derives from
|
|
@@ -16,14 +17,13 @@ import { mkdir, rm, writeFile } from 'node:fs/promises'
|
|
|
16
17
|
import { dirname, join } from 'node:path'
|
|
17
18
|
import { parseConfig } from '../config/load'
|
|
18
19
|
import type { VerifyStepConfig } from '../config/schema'
|
|
19
|
-
import type { AbEvent } from '../events/catalog'
|
|
20
20
|
import {
|
|
21
21
|
allowedTerminals,
|
|
22
22
|
phaseSpecFor,
|
|
23
23
|
type PhaseSpec,
|
|
24
24
|
type TerminalCommand,
|
|
25
25
|
} from '../kernel/phases'
|
|
26
|
-
import {
|
|
26
|
+
import type { BuildState } from '../kernel/reducer'
|
|
27
27
|
import {
|
|
28
28
|
isVerifyPhase,
|
|
29
29
|
verifyReportKind,
|
|
@@ -113,69 +113,12 @@ function requiredKinds(spec: PhaseSpec, phase: Phase): string[] {
|
|
|
113
113
|
|
|
114
114
|
/** All finding objects ever deposited, by id — verdict payloads carry them in
|
|
115
115
|
* full (§15.3), so feedback-by-id resolves without touching artifacts. */
|
|
116
|
-
function findingsById(events:
|
|
117
|
-
|
|
118
|
-
for (const event of events) {
|
|
119
|
-
if (event.type === 'plan-review.verdict' || event.type === 'code-review.verdict') {
|
|
120
|
-
for (const finding of event.payload.findings) byId.set(finding.id, finding)
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return byId
|
|
116
|
+
function findingsById(events: CurrentBuildFacts): Map<string, Finding> {
|
|
117
|
+
return new Map(Object.values(events.findings).map((finding) => [finding.id, finding]))
|
|
124
118
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
* `implement.started`, `plan.started`, and agent `verify.started` may carry
|
|
129
|
-
* feedback — the carrier that gets answered guidance to a fresh session after
|
|
130
|
-
* a parked build re-attaches (§15.6-B). A `plan.started` without one (logs
|
|
131
|
-
* predating the field) falls
|
|
132
|
-
* back to deriving round-N feedback from the round-(N-1)
|
|
133
|
-
* `plan-review.verdict` — the plan loop's verdicts ARE its findings channel
|
|
134
|
-
* (§8.3: "prior plan rev + findings").
|
|
135
|
-
*/
|
|
136
|
-
function currentFeedback(events: AbEvent[], phase: Phase, round: number): Feedback | null {
|
|
137
|
-
if (isVerifyPhase(phase)) {
|
|
138
|
-
const step = verifyStep(phase)
|
|
139
|
-
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
140
|
-
const event = events[i]!
|
|
141
|
-
if (
|
|
142
|
-
event.type === 'verify.started' &&
|
|
143
|
-
event.payload.step === step &&
|
|
144
|
-
event.payload.attempt === round
|
|
145
|
-
) {
|
|
146
|
-
return event.payload.feedback ?? null
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return null
|
|
150
|
-
}
|
|
151
|
-
if (phase === 'implement') {
|
|
152
|
-
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
153
|
-
const event = events[i]!
|
|
154
|
-
if (event.type === 'implement.started' && event.payload.round === round) {
|
|
155
|
-
return event.payload.feedback ?? null
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return null
|
|
159
|
-
}
|
|
160
|
-
if (phase === 'plan') {
|
|
161
|
-
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
162
|
-
const event = events[i]!
|
|
163
|
-
if (event.type === 'plan.started' && event.payload.round === round) {
|
|
164
|
-
if (event.payload.feedback !== undefined) return event.payload.feedback
|
|
165
|
-
break // legacy start without the field — derive from the prior verdict
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
if (round <= 1) return null
|
|
169
|
-
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
170
|
-
const event = events[i]!
|
|
171
|
-
if (event.type === 'plan-review.verdict' && event.payload.round === round - 1) {
|
|
172
|
-
return event.payload.verdict === 'revise'
|
|
173
|
-
? { findings: event.payload.findings.map((finding) => finding.id) }
|
|
174
|
-
: null
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return null
|
|
119
|
+
function currentFeedback(events: CurrentBuildFacts, phase: Phase, round: number): Feedback | null {
|
|
120
|
+
const explicit = events.feedback[JSON.stringify([phase, round])]
|
|
121
|
+
return explicit ?? (phase === 'plan' ? (events.planFeedback[String(round)] ?? null) : null)
|
|
179
122
|
}
|
|
180
123
|
|
|
181
124
|
/** Empty and recreate `.ab/`; callers restore any tracked legacy entries. */
|
|
@@ -184,7 +127,7 @@ async function wipeAbDir(abDir: string): Promise<void> {
|
|
|
184
127
|
await mkdir(abDir, { recursive: true })
|
|
185
128
|
}
|
|
186
129
|
|
|
187
|
-
function dismissedIds(state: BuildState, events:
|
|
130
|
+
function dismissedIds(state: BuildState, events: CurrentBuildFacts): string[] {
|
|
188
131
|
const known = findingsById(events)
|
|
189
132
|
const ids = new Set<string>()
|
|
190
133
|
for (const escalation of state.answeredEscalations) {
|
|
@@ -205,8 +148,8 @@ export async function buildContext(deps: ContextDeps): Promise<ContextManifest>
|
|
|
205
148
|
const { build, phase, round } = env
|
|
206
149
|
const spec = phaseSpecFor(phase)
|
|
207
150
|
const inputs = spec.inputs
|
|
208
|
-
const events = await store
|
|
209
|
-
const state =
|
|
151
|
+
const events = (await readBuildFacts(store, build)).value
|
|
152
|
+
const state = events.state
|
|
210
153
|
|
|
211
154
|
// Preserve legacy tracked scratch exactly while replacing disposable files.
|
|
212
155
|
// Some adapter contract fixtures intentionally use a non-Git workspace;
|
|
@@ -271,9 +214,15 @@ export async function buildContext(deps: ContextDeps): Promise<ContextManifest>
|
|
|
271
214
|
}
|
|
272
215
|
|
|
273
216
|
if (inputs.ticket === true) {
|
|
274
|
-
const created = events.
|
|
217
|
+
const created = events.created
|
|
275
218
|
if (created !== undefined && created.type === 'build.created') {
|
|
276
|
-
const
|
|
219
|
+
const original = created.payload.ticket
|
|
220
|
+
const current =
|
|
221
|
+
original.source === 'hosted' ? (await store.getBuild(build))?.ticket : undefined
|
|
222
|
+
const ticket = {
|
|
223
|
+
...original,
|
|
224
|
+
...(current?.source === original.source ? { id: current.id } : {}),
|
|
225
|
+
}
|
|
277
226
|
const lines = [
|
|
278
227
|
`# Ticket: ${ticket.title ?? ticket.id}`,
|
|
279
228
|
'',
|
|
@@ -436,13 +385,8 @@ export async function buildContext(deps: ContextDeps): Promise<ContextManifest>
|
|
|
436
385
|
// records the execution input in reconcile.started. A crashed same-attempt
|
|
437
386
|
// rerun can therefore have multiple starts; the newest matching AB_PHASE
|
|
438
387
|
// attempt is authoritative.
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (event.type === 'reconcile.started' && event.payload.attempt === round) {
|
|
442
|
-
manifest.conflict = { baseSha: event.payload.baseSha }
|
|
443
|
-
break
|
|
444
|
-
}
|
|
445
|
-
}
|
|
388
|
+
const baseSha = events.reconcileBases[String(round)]
|
|
389
|
+
if (baseSha !== undefined) manifest.conflict = { baseSha }
|
|
446
390
|
if (manifest.conflict === undefined) {
|
|
447
391
|
throw new Error(
|
|
448
392
|
`reconcile@${round} context requires a matching reconcile.started event ` +
|
|
@@ -482,10 +426,10 @@ export async function buildContext(deps: ContextDeps): Promise<ContextManifest>
|
|
|
482
426
|
// Legacy tracked scratch is repository debt, not content Autobuild rewrites.
|
|
483
427
|
// Record it once per build from the provisioned base so harvest can groom it.
|
|
484
428
|
if (gitWorkspace && deps.ids !== undefined) {
|
|
485
|
-
const provisioned = events.
|
|
429
|
+
const provisioned = events.firstWorkspace
|
|
486
430
|
const marker = 'phase-scratch:tracked-at-provisioned-base'
|
|
487
|
-
const alreadyRecorded = events.some(
|
|
488
|
-
|
|
431
|
+
const alreadyRecorded = events.observations.some((observation) =>
|
|
432
|
+
observation.refs?.includes(marker),
|
|
489
433
|
)
|
|
490
434
|
if (
|
|
491
435
|
provisioned !== undefined &&
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { RepositoryReadFacts } from '../../store/reduced-reads'
|
|
1
2
|
/**
|
|
2
3
|
* The dashboard's pure projection: `BuildState` → what the operator sees.
|
|
3
4
|
*
|
|
@@ -920,7 +921,7 @@ function escalationAttentionDismissed(events: RepositoryEvent[], terminalSeq: nu
|
|
|
920
921
|
}
|
|
921
922
|
|
|
922
923
|
function selectDashboardHarvestRun(
|
|
923
|
-
events: RepositoryEvent[],
|
|
924
|
+
events: RepositoryEvent[] | RepositoryReadFacts,
|
|
924
925
|
state: HarvestState,
|
|
925
926
|
): HarvestRunState | undefined {
|
|
926
927
|
const attention = state.runs.find((run) => {
|
|
@@ -928,7 +929,12 @@ function selectDashboardHarvestRun(
|
|
|
928
929
|
return run.recoveryExhaustion?.attentionAcknowledgedSeq === undefined
|
|
929
930
|
}
|
|
930
931
|
if (run.status !== 'escalated') return false
|
|
931
|
-
return
|
|
932
|
+
return (
|
|
933
|
+
run.terminalSeq === undefined ||
|
|
934
|
+
!(Array.isArray(events)
|
|
935
|
+
? escalationAttentionDismissed(events, run.terminalSeq)
|
|
936
|
+
: events.dismissedThrough > run.terminalSeq)
|
|
937
|
+
)
|
|
932
938
|
})
|
|
933
939
|
return attention ?? openHarvestRun(state)
|
|
934
940
|
}
|
|
@@ -937,7 +943,7 @@ function selectDashboardHarvestRun(
|
|
|
937
943
|
* exhausted, or escalated attention outranks merely open work; completed and
|
|
938
944
|
* acknowledged terminal history never hides an older actionable run. */
|
|
939
945
|
function projectHarvestRun(
|
|
940
|
-
events: RepositoryEvent[],
|
|
946
|
+
events: RepositoryEvent[] | RepositoryReadFacts,
|
|
941
947
|
state: HarvestState,
|
|
942
948
|
): DashboardHarvest | undefined {
|
|
943
949
|
const run = selectDashboardHarvestRun(events, state)
|
|
@@ -968,25 +974,27 @@ function projectHarvestRun(
|
|
|
968
974
|
const approved = run.reviews.some((review) => review.verdict === 'approve')
|
|
969
975
|
const synthOutput = run.proposals.length > 0
|
|
970
976
|
const reviewOutput = run.reviews.length > 0
|
|
971
|
-
const
|
|
972
|
-
run.status
|
|
973
|
-
?
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
(
|
|
977
|
-
|
|
978
|
-
event
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
977
|
+
const failureStoppedAt =
|
|
978
|
+
run.status !== 'failed'
|
|
979
|
+
? undefined
|
|
980
|
+
: Array.isArray(events)
|
|
981
|
+
? [...events]
|
|
982
|
+
.reverse()
|
|
983
|
+
.find(
|
|
984
|
+
(event) =>
|
|
985
|
+
event.type === 'harvest.failed' &&
|
|
986
|
+
event.payload.run === run.run &&
|
|
987
|
+
!event.payload.willRetry,
|
|
988
|
+
)?.ts
|
|
989
|
+
: events.failedAt[JSON.stringify(run.run)]
|
|
982
990
|
// Both durable stop states freeze any open occurrence. A failure normally
|
|
983
991
|
// closes its step immediately afterward, but projecting the boundary itself
|
|
984
992
|
// must never make an errored row look like work is still accumulating time.
|
|
985
993
|
const frozenAt =
|
|
986
994
|
state.pausedAt !== undefined
|
|
987
995
|
? Date.parse(state.pausedAt)
|
|
988
|
-
:
|
|
989
|
-
? Date.parse(
|
|
996
|
+
: failureStoppedAt !== undefined
|
|
997
|
+
? Date.parse(failureStoppedAt)
|
|
990
998
|
: undefined
|
|
991
999
|
const steps: PipelineStep[] = [
|
|
992
1000
|
step('scan', hasOutcome('scan', 'completed'), current('scan'), {
|
|
@@ -1073,8 +1081,10 @@ interface RepositoryHarvestProjection {
|
|
|
1073
1081
|
/** The repository-journal half of a legacy dashboard frame (AUT-545): the
|
|
1074
1082
|
* harvest projection and the pause flag exactly as `buildDashboardFromProjected`
|
|
1075
1083
|
* derives them, exposed for tests that pin the bounded read's equivalence. */
|
|
1076
|
-
export function projectRepositoryHarvest(
|
|
1077
|
-
|
|
1084
|
+
export function projectRepositoryHarvest(
|
|
1085
|
+
events: RepositoryEvent[] | RepositoryReadFacts,
|
|
1086
|
+
): RepositoryHarvestProjection {
|
|
1087
|
+
const state = Array.isArray(events) ? reduceHarvest(events) : events.harvest
|
|
1078
1088
|
const harvest = projectHarvestRun(events, state)
|
|
1079
1089
|
return {
|
|
1080
1090
|
harvestPaused: state.paused,
|
|
@@ -1082,7 +1092,9 @@ export function projectRepositoryHarvest(events: RepositoryEvent[]): RepositoryH
|
|
|
1082
1092
|
}
|
|
1083
1093
|
}
|
|
1084
1094
|
|
|
1085
|
-
export function projectHarvest(
|
|
1095
|
+
export function projectHarvest(
|
|
1096
|
+
events: RepositoryEvent[] | RepositoryReadFacts,
|
|
1097
|
+
): DashboardHarvest | undefined {
|
|
1086
1098
|
return projectRepositoryHarvest(events).harvest
|
|
1087
1099
|
}
|
|
1088
1100
|
|
|
@@ -1112,11 +1124,13 @@ export interface DashboardHeader extends DashboardFrameHeader {
|
|
|
1112
1124
|
export function buildDashboardFromProjected(
|
|
1113
1125
|
projectedBuilds: readonly DashboardBuild[],
|
|
1114
1126
|
header: DashboardHeader,
|
|
1115
|
-
repositoryEvents: RepositoryEvent[] = [],
|
|
1127
|
+
repositoryEvents: RepositoryEvent[] | RepositoryReadFacts = [],
|
|
1116
1128
|
): DashboardModel {
|
|
1117
1129
|
const builds = [...projectedBuilds].sort((a, b) => a.slug.localeCompare(b.slug))
|
|
1118
1130
|
const harvestProjection = projectRepositoryHarvest(repositoryEvents)
|
|
1119
|
-
const settings =
|
|
1131
|
+
const settings = Array.isArray(repositoryEvents)
|
|
1132
|
+
? reduceDispatchSettings(repositoryEvents)
|
|
1133
|
+
: repositoryEvents.settings
|
|
1120
1134
|
return {
|
|
1121
1135
|
repo: header.repo,
|
|
1122
1136
|
queued: header.queued,
|
|
@@ -1143,7 +1157,7 @@ export function buildDashboard(
|
|
|
1143
1157
|
entries: { record: BuildRecord; state: BuildState; events: AbEvent[] }[],
|
|
1144
1158
|
config: Config,
|
|
1145
1159
|
header: DashboardFrameHeader,
|
|
1146
|
-
repositoryEvents: RepositoryEvent[] = [],
|
|
1160
|
+
repositoryEvents: RepositoryEvent[] | RepositoryReadFacts = [],
|
|
1147
1161
|
): DashboardModel {
|
|
1148
1162
|
const builds = entries
|
|
1149
1163
|
.map(({ record, state, events }) => projectBuild(record, state, config, events))
|
|
@@ -211,7 +211,7 @@ export class DashboardBuildPollCache {
|
|
|
211
211
|
// both the cached reduction and projected row untouched.
|
|
212
212
|
next.set(
|
|
213
213
|
record.slug,
|
|
214
|
-
configChanged
|
|
214
|
+
configChanged || current.build?.ticketId !== record.ticket?.id
|
|
215
215
|
? {
|
|
216
216
|
...current,
|
|
217
217
|
build: await this.projectRow(record, current.state, current.events, config),
|