@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.
- package/README.md +1 -1
- package/SPEC.md +19 -1
- package/bin/ab-dispatch-kernel.ts +4 -6
- package/docs/architecture.md +96 -70
- package/docs/hosted-service.md +2 -2
- package/docs/operator-api.md +1 -1
- package/docs/remote-store-protocol.md +84 -2
- package/package.json +4 -3
- package/packages/core/src/cli/attention.ts +18 -164
- package/packages/core/src/cli/build-control.ts +11 -15
- package/packages/core/src/cli/bulk-control.ts +3 -2
- package/packages/core/src/cli/context.ts +16 -78
- package/packages/core/src/cli/dashboard/model.ts +36 -22
- 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 +148 -79
- package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
- package/packages/core/src/cli/harvest.ts +71 -39
- package/packages/core/src/cli/historical-build-state.ts +27 -0
- package/packages/core/src/cli/main.ts +3 -3
- 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/status.ts +62 -32
- package/packages/core/src/cli/terminals.ts +25 -142
- package/packages/core/src/cli/ticket.ts +2 -2
- 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/events/repository.ts +7 -3
- package/packages/core/src/integration/harness.ts +1 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2429 -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/repository-journals.ts +14 -3
- package/packages/core/src/kernel/harvest.ts +25 -23
- 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 +137 -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/operator/control.ts +5 -6
- package/packages/core/src/operator/index.ts +2 -0
- package/packages/core/src/operator/query.ts +37 -20
- package/packages/core/src/operator/read-view.ts +4 -13
- package/packages/core/src/operator/requests.ts +2 -14
- package/packages/core/src/operator/sandbox.ts +10 -9
- package/packages/core/src/operator/tickets.ts +2 -2
- package/packages/core/src/plugin-sdk/index.ts +13 -0
- 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 +113 -173
- package/packages/core/src/processes/harvest-execution-state.ts +15 -9
- package/packages/core/src/processes/harvest-runner.ts +34 -70
- package/packages/core/src/processes/harvest.ts +38 -15
- 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/publication-loss.ts +35 -21
- package/packages/core/src/processes/repo-view.ts +229 -1134
- 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 +251 -0
- package/packages/core/src/store/local/schema.ts +1 -0
- package/packages/core/src/store/local/store.ts +179 -54
- 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-assets.ts +4 -4
- package/packages/core/src/store/types.ts +39 -2
- 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 +6 -1
- package/skills/guide/references/operator-api.md +1 -1
- package/skills/guide/references/remote-store-protocol.md +84 -2
- package/templates/hosted-service/package.json +2 -2
|
@@ -1,30 +1,30 @@
|
|
|
1
|
+
import { readRepositoryFacts, readDispatcherRunFacts } from '../store/reduced-reads'
|
|
1
2
|
import type { RepositoryEvent } from '../events/repository'
|
|
2
3
|
import type { BuildStore, RepositoryRecord } from '../store/types'
|
|
3
4
|
import { formatDispatcherIdentity, projectDispatcherLiveness } from '../kernel/dispatcher-liveness'
|
|
4
5
|
|
|
5
|
-
/**
|
|
6
|
-
* an earlier stop. Only this correlation query falls back to full history. */
|
|
6
|
+
/** Correlate only the latest, stopped and lease-holder runs through persisted values. */
|
|
7
7
|
export async function readDispatcherLiveness(
|
|
8
8
|
store: BuildStore,
|
|
9
9
|
repo: string,
|
|
10
10
|
now = new Date(),
|
|
11
|
-
|
|
11
|
+
_subset?: RepositoryEvent[],
|
|
12
12
|
) {
|
|
13
13
|
const record = await store.getRepo(repo)
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
const facts = record === null ? undefined : (await readRepositoryFacts(store, repo)).value
|
|
15
|
+
const runs = new Set(
|
|
16
|
+
[facts?.latestRun, facts?.latestStoppedRun, record?.lease?.holder].filter(
|
|
17
|
+
(run): run is string => run !== undefined,
|
|
18
|
+
),
|
|
19
|
+
)
|
|
20
|
+
const correlations = await Promise.all(
|
|
21
|
+
[...runs].map(async (run) => (await readDispatcherRunFacts(store, repo, run)).value),
|
|
22
|
+
)
|
|
23
|
+
const events = correlations.flatMap((run) =>
|
|
24
|
+
[run.start, run.latestStart, run.stop, run.tick].filter(
|
|
25
|
+
(event): event is RepositoryEvent => event !== undefined,
|
|
26
|
+
),
|
|
27
|
+
)
|
|
28
28
|
return { record, events, dispatcher: projectDispatcherLiveness(record, events, now) }
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -39,6 +39,10 @@ export class StandbyIdentityResolver {
|
|
|
39
39
|
constructor(
|
|
40
40
|
private readonly store: BuildStore,
|
|
41
41
|
private readonly repo: string,
|
|
42
|
+
private readonly history?: {
|
|
43
|
+
allowLookup: () => boolean
|
|
44
|
+
remember: (events: RepositoryEvent[], complete: boolean) => void
|
|
45
|
+
},
|
|
42
46
|
) {}
|
|
43
47
|
|
|
44
48
|
ingest(events: readonly RepositoryEvent[]): void {
|
|
@@ -51,11 +55,14 @@ export class StandbyIdentityResolver {
|
|
|
51
55
|
const pending = this.pending.get(holder)
|
|
52
56
|
if (pending !== undefined) return pending
|
|
53
57
|
if (this.starts.has(holder) || this.attempted.has(holder)) return
|
|
58
|
+
if (this.history && !this.history.allowLookup()) return
|
|
54
59
|
this.attempted.add(holder)
|
|
55
60
|
const lookup = (async () => {
|
|
56
61
|
try {
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
const facts = (await readDispatcherRunFacts(this.store, this.repo, holder)).value
|
|
63
|
+
this.ingest(facts.start ? [facts.start] : [])
|
|
64
|
+
const complete = false // This lookup correlates one holder, not the complete journal.
|
|
65
|
+
this.history?.remember([...this.starts.values()], complete)
|
|
59
66
|
} catch {
|
|
60
67
|
// Identity is best effort; the sampled holder id remains sufficient.
|
|
61
68
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readRepositoryFacts, type RepositoryReadFacts } from '../store/reduced-reads'
|
|
1
2
|
/** Typed agent/operator surface for repository-scoped harvest workflows. Agent
|
|
2
3
|
* stdout is never interpreted: proposal sets and review verdicts land through
|
|
3
4
|
* this module as validated repository events and versioned artifacts. */
|
|
@@ -67,8 +68,8 @@ async function writeJson(path: string, value: unknown): Promise<void> {
|
|
|
67
68
|
await writeFile(path, `${JSON.stringify(value, null, 2)}\n`)
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
function runForEnv(events:
|
|
71
|
-
const run =
|
|
71
|
+
function runForEnv(events: RepositoryReadFacts, env: HarvestCliEnv): HarvestRunState {
|
|
72
|
+
const run = events.harvest.runs.find((candidate) => candidate.run === env.run)
|
|
72
73
|
if (!run) throw new Error(`unknown harvest run "${env.run}" in repo "${env.repo}"`)
|
|
73
74
|
if (run.status !== 'running') {
|
|
74
75
|
throw new Error(
|
|
@@ -78,34 +79,44 @@ function runForEnv(events: RepositoryEvent[], env: HarvestCliEnv): HarvestRunSta
|
|
|
78
79
|
return run
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
function assertNoSessionTerminal(events:
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
82
|
+
function assertNoSessionTerminal(events: RepositoryReadFacts, env: HarvestCliEnv): void {
|
|
83
|
+
const run = events.harvest.runs.find((run) => run.run === env.run)
|
|
84
|
+
const proposal =
|
|
85
|
+
env.phase === 'synthesize'
|
|
86
|
+
? run?.proposals.find((proposal) => proposal.round === env.round)
|
|
87
|
+
: undefined
|
|
88
|
+
const review =
|
|
89
|
+
env.phase === 'review'
|
|
90
|
+
? events.harvestVerdicts[JSON.stringify([env.run, env.round])]?.first
|
|
91
|
+
: undefined
|
|
92
|
+
const ended = events.harvestSessionEndings[JSON.stringify(env.session)]
|
|
93
|
+
const errors = [
|
|
94
|
+
...(proposal
|
|
95
|
+
? [
|
|
96
|
+
{
|
|
97
|
+
seq: proposal.seq,
|
|
98
|
+
message: `second harvest terminal rejected: proposals for ${env.run} synthesize@${env.round} already landed at repo seq ${proposal.seq}`,
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
: []),
|
|
102
|
+
...(review
|
|
103
|
+
? [
|
|
104
|
+
{
|
|
105
|
+
seq: review.seq,
|
|
106
|
+
message: `second harvest terminal rejected: verdict for ${env.run} review@${env.round} already landed at repo seq ${review.seq}`,
|
|
107
|
+
},
|
|
108
|
+
]
|
|
109
|
+
: []),
|
|
110
|
+
...(ended
|
|
111
|
+
? [
|
|
112
|
+
{
|
|
113
|
+
seq: ended,
|
|
114
|
+
message: `harvest terminal rejected: session "${env.session}" already ended at repo seq ${ended}`,
|
|
115
|
+
},
|
|
116
|
+
]
|
|
117
|
+
: []),
|
|
118
|
+
].sort((a, b) => a.seq - b.seq)
|
|
119
|
+
if (errors[0]) throw new Error(errors[0].message)
|
|
109
120
|
}
|
|
110
121
|
|
|
111
122
|
async function readArtifactJson(
|
|
@@ -122,7 +133,7 @@ async function readArtifactJson(
|
|
|
122
133
|
export async function buildHarvestContext(deps: HarvestCliDeps): Promise<HarvestContextManifest> {
|
|
123
134
|
const { store, env, workspacePath } = deps
|
|
124
135
|
// Bounded read (AUT-489): runForEnv reduces harvest facts only.
|
|
125
|
-
const events = await store
|
|
136
|
+
const events = (await readRepositoryFacts(store, env.repo)).value
|
|
126
137
|
const run = runForEnv(events, env)
|
|
127
138
|
const packet = await loadScanPacket(store, env.repo, run.scan)
|
|
128
139
|
const abDir = join(workspacePath, '.ab')
|
|
@@ -230,7 +241,7 @@ export async function submitHarvestProposals(
|
|
|
230
241
|
}
|
|
231
242
|
// Bounded read (AUT-489): runForEnv and assertNoSessionTerminal consume
|
|
232
243
|
// harvest facts only.
|
|
233
|
-
const events = await store
|
|
244
|
+
const events = (await readRepositoryFacts(store, env.repo)).value
|
|
234
245
|
const run = runForEnv(events, env)
|
|
235
246
|
assertNoSessionTerminal(events, env)
|
|
236
247
|
|
|
@@ -322,7 +333,7 @@ export async function submitHarvestVerdict(
|
|
|
322
333
|
}
|
|
323
334
|
// Bounded read (AUT-489): runForEnv and assertNoSessionTerminal consume
|
|
324
335
|
// harvest facts only.
|
|
325
|
-
const events = await store
|
|
336
|
+
const events = (await readRepositoryFacts(store, env.repo)).value
|
|
326
337
|
const run = runForEnv(events, env)
|
|
327
338
|
assertNoSessionTerminal(events, env)
|
|
328
339
|
if (proposalArtifactForRound(run, env.round) === undefined) {
|
|
@@ -602,19 +613,35 @@ function projectHarvestRunStatus(run: HarvestRunState): Omit<HarvestRunStatusVie
|
|
|
602
613
|
|
|
603
614
|
export function projectHarvestStatus(
|
|
604
615
|
repo: string,
|
|
605
|
-
events: RepositoryEvent[],
|
|
616
|
+
events: RepositoryEvent[] | RepositoryReadFacts,
|
|
606
617
|
newestEvents?: number,
|
|
607
618
|
streams?: readonly StreamRecord[],
|
|
608
619
|
): HarvestStatusView {
|
|
609
|
-
const state = reduceHarvest(events)
|
|
620
|
+
const state = Array.isArray(events) ? reduceHarvest(events) : events.harvest
|
|
610
621
|
const history =
|
|
611
|
-
newestEvents === undefined
|
|
622
|
+
newestEvents === undefined || !Array.isArray(events)
|
|
623
|
+
? undefined
|
|
624
|
+
: events.filter(isHarvestEvent).slice(-newestEvents)
|
|
612
625
|
const unresolved = [...parkedHarvestRuns(state), ...unresolvedHarvestAttentionRuns(state)]
|
|
613
626
|
const open = openHarvestRuns(state)
|
|
614
627
|
const included = new Set(
|
|
615
628
|
[...unresolved, ...open, ...(state.latest ? [state.latest] : [])].map((run) => run.run),
|
|
616
629
|
)
|
|
617
|
-
const sessionsByRun =
|
|
630
|
+
const sessionsByRun = Array.isArray(events)
|
|
631
|
+
? projectHarvestSessions(events, streams)
|
|
632
|
+
: new Map<string, HarvestSessionStatusView[]>()
|
|
633
|
+
if (!Array.isArray(events))
|
|
634
|
+
for (const { run, ...session } of events.harvestSessions) {
|
|
635
|
+
const record = streams?.find(
|
|
636
|
+
(record) => record.scope.kind === 'repo' && record.label === `session:${session.session}`,
|
|
637
|
+
)
|
|
638
|
+
const list = sessionsByRun.get(run) ?? []
|
|
639
|
+
list.push({
|
|
640
|
+
...session,
|
|
641
|
+
...(record ? { stream: record.id, streamStatus: record.status } : {}),
|
|
642
|
+
})
|
|
643
|
+
sessionsByRun.set(run, list)
|
|
644
|
+
}
|
|
618
645
|
const runs = state.runs
|
|
619
646
|
.filter((run) => included.has(run.run))
|
|
620
647
|
.map((run) => ({
|
|
@@ -802,7 +829,7 @@ export async function abHarvestStatus(opts: HarvestStatusOpts): Promise<void> {
|
|
|
802
829
|
async ({ store, repo }) => {
|
|
803
830
|
const record = await store.getRepo(repo)
|
|
804
831
|
// Bounded read (AUT-489): projectHarvestStatus reduces harvest facts only.
|
|
805
|
-
const events =
|
|
832
|
+
const events = (await readRepositoryFacts(store, repo)).value
|
|
806
833
|
// Best-effort stream enrichment for the session rows, mirroring the
|
|
807
834
|
// degrade-gracefully pattern of 'ab build status': a store that cannot
|
|
808
835
|
// list streams keeps the payload-derived projection instead of failing.
|
|
@@ -814,7 +841,12 @@ export async function abHarvestStatus(opts: HarvestStatusOpts): Promise<void> {
|
|
|
814
841
|
streams = undefined
|
|
815
842
|
}
|
|
816
843
|
}
|
|
817
|
-
const view = projectHarvestStatus(repo, events,
|
|
844
|
+
const view = projectHarvestStatus(repo, events, undefined, streams)
|
|
845
|
+
if (opts.events !== undefined)
|
|
846
|
+
view.events =
|
|
847
|
+
record === null
|
|
848
|
+
? []
|
|
849
|
+
: (await store.getRepoEvents(repo)).filter(isHarvestEvent).slice(-opts.events)
|
|
818
850
|
if (opts.json === true) opts.stdout(JSON.stringify(view, null, 2))
|
|
819
851
|
else for (const line of renderHarvestStatus(view)) opts.stdout(line)
|
|
820
852
|
},
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { BuildStore } from '../store/types'
|
|
2
|
+
import { buildStreamSeedReducer, type BuildStreamSeed } from '../kernel/read-facts'
|
|
3
|
+
|
|
4
|
+
/** Only explicit old --since cursors need historical reconstruction. Ordinary
|
|
5
|
+
* registration and the first event batch must use the persisted seed. */
|
|
6
|
+
export async function historicalBuildState(
|
|
7
|
+
store: BuildStore,
|
|
8
|
+
slug: string,
|
|
9
|
+
requested: number,
|
|
10
|
+
current: { cursor: number; value: BuildStreamSeed },
|
|
11
|
+
) {
|
|
12
|
+
if (requested >= current.cursor) return current.value
|
|
13
|
+
const events = await store.getEvents(slug)
|
|
14
|
+
return buildStreamSeedReducer.reduce(events.filter((event) => event.seq <= requested))
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
import { repositoryAttentionReducer } from '../kernel/repository-attention'
|
|
18
|
+
export async function historicalRepositoryAttention(
|
|
19
|
+
store: BuildStore,
|
|
20
|
+
repo: string,
|
|
21
|
+
requested: number,
|
|
22
|
+
current: { cursor: number; value: ReturnType<typeof repositoryAttentionReducer.finish> },
|
|
23
|
+
) {
|
|
24
|
+
if (requested >= current.cursor) return current.value
|
|
25
|
+
const events = await store.getRepoEvents(repo)
|
|
26
|
+
return repositoryAttentionReducer.reduce(events.filter((event) => event.seq <= requested))
|
|
27
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readBuildFacts } from '../store/reduced-reads'
|
|
1
2
|
import { preparePinnedAssets } from '../store/ticket-assets'
|
|
2
3
|
import type { PinnedAsset } from '../events/payloads'
|
|
3
4
|
import { abConnect, abDisconnect } from './connect'
|
|
@@ -21,7 +22,6 @@ import { textContent } from '../store/types'
|
|
|
21
22
|
import type { Forge } from '../ports/types'
|
|
22
23
|
import { spawnExec, type Exec } from '../ports/workspace/git-worktree'
|
|
23
24
|
import type { IdSource } from '../ids'
|
|
24
|
-
import { reduceBuild } from '../kernel/reducer'
|
|
25
25
|
import { artifactDownload, artifactDownloadStream, artifactGet, artifactPut } from './artifact'
|
|
26
26
|
import { multiFlag, parseArgs, stringFlag, type ParsedArgs } from './args'
|
|
27
27
|
import { abBuildControl, type BuildControlAction, type BuildControlResult } from './build-control'
|
|
@@ -1372,8 +1372,8 @@ async function dispatch(argv: string[], deps: SessionlessCliDeps): Promise<numbe
|
|
|
1372
1372
|
// external side effect is best-effort after the atomic designation.
|
|
1373
1373
|
if (attach) {
|
|
1374
1374
|
try {
|
|
1375
|
-
const events = await session.store
|
|
1376
|
-
const pr =
|
|
1375
|
+
const events = (await readBuildFacts(session.store, session.env.build)).value
|
|
1376
|
+
const pr = events.state.pr
|
|
1377
1377
|
if (pr !== undefined) {
|
|
1378
1378
|
try {
|
|
1379
1379
|
await preparePrAttachments(session, events, pr.url)
|
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { CurrentBuildFacts } from '../store/reduced-reads'
|
|
2
|
+
import type { EventEnvelope } from '../events/catalog'
|
|
2
3
|
import { KERNEL } from '../events/envelope'
|
|
3
4
|
import type { IdSource } from '../ids'
|
|
4
|
-
import {
|
|
5
|
-
currentPrAttachments,
|
|
6
|
-
frozenPrImageHost,
|
|
7
|
-
hostedPrAttachments,
|
|
8
|
-
} from '../kernel/pr-attachments'
|
|
5
|
+
import type { currentPrAttachments } from '../kernel/pr-attachments'
|
|
9
6
|
import { hostedPrAttachmentAssetSchema, type HostedPrAttachmentAsset } from '../ontology'
|
|
10
7
|
import type { Forge } from '../ports/types'
|
|
11
8
|
import type { BuildStore } from '../store/types'
|
|
@@ -25,7 +22,7 @@ function errorMessage(error: unknown): string {
|
|
|
25
22
|
|
|
26
23
|
async function recordHostingFailure(
|
|
27
24
|
deps: PrAttachmentDeps,
|
|
28
|
-
events:
|
|
25
|
+
events: CurrentBuildFacts,
|
|
29
26
|
designation: ReturnType<typeof currentPrAttachments>[number],
|
|
30
27
|
error: unknown,
|
|
31
28
|
extraRefs: string[] = [],
|
|
@@ -44,7 +41,7 @@ async function recordHostingFailure(
|
|
|
44
41
|
refs: [`${artifact.kind}@${artifact.rev}`, ...extraRefs],
|
|
45
42
|
},
|
|
46
43
|
})
|
|
47
|
-
events.push(observed)
|
|
44
|
+
events.observations.push(observed.payload)
|
|
48
45
|
} catch {
|
|
49
46
|
// Hosting is a best-effort review affordance. A secondary failure to
|
|
50
47
|
// record its follow-up must not hide the complete text projection or turn
|
|
@@ -61,14 +58,14 @@ async function recordHostingFailure(
|
|
|
61
58
|
*/
|
|
62
59
|
export async function preparePrAttachments(
|
|
63
60
|
deps: PrAttachmentDeps,
|
|
64
|
-
events:
|
|
61
|
+
events: CurrentBuildFacts,
|
|
65
62
|
prUrl: string,
|
|
66
63
|
): Promise<void> {
|
|
67
|
-
const target =
|
|
64
|
+
const target = events.imageHost
|
|
68
65
|
if (target === undefined) return
|
|
69
66
|
|
|
70
|
-
const designations =
|
|
71
|
-
const hosted =
|
|
67
|
+
const designations = events.designations
|
|
68
|
+
const hosted = new Map(events.hostedImages)
|
|
72
69
|
for (const designation of designations) {
|
|
73
70
|
if (!designation.payload.mediaType.startsWith('image/')) continue
|
|
74
71
|
if (hosted.has(designation.seq)) continue
|
|
@@ -132,7 +129,7 @@ export async function preparePrAttachments(
|
|
|
132
129
|
)
|
|
133
130
|
throw error
|
|
134
131
|
}
|
|
135
|
-
events.push(event)
|
|
132
|
+
events.hostedImages.push([designation.seq, event])
|
|
136
133
|
hosted.set(designation.seq, event)
|
|
137
134
|
}
|
|
138
135
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readBuildFacts, type CurrentBuildFacts } from '../store/reduced-reads'
|
|
2
2
|
export { summaryBoundary, pendingSummaryRequests } from '../kernel/pr-summary'
|
|
3
3
|
import type { AbEvent } from '../events/catalog'
|
|
4
4
|
import { KERNEL } from '../events/envelope'
|
|
5
5
|
import { isRemoteWorkspace } from '../events/workspace-remote'
|
|
6
|
-
import { reduceBuild } from '../kernel/reducer'
|
|
7
6
|
import { currentPrAttachments } from '../kernel/pr-attachments'
|
|
8
|
-
import { openExecution } from '../processes/execution-settlement'
|
|
9
7
|
import type { Forge } from '../ports/types'
|
|
10
8
|
import { githubRateLimitUntil } from '../ports/forge/github-transport'
|
|
11
9
|
import { summaryMarker } from '../ports/forge/summary-marker'
|
|
@@ -27,7 +25,7 @@ interface SummaryDeps {
|
|
|
27
25
|
* is deliberately not designated, avoiding recursive growth. */
|
|
28
26
|
export async function boundedSummary(
|
|
29
27
|
deps: SummaryDeps,
|
|
30
|
-
events: readonly AbEvent[],
|
|
28
|
+
events: readonly AbEvent[] | CurrentBuildFacts,
|
|
31
29
|
): Promise<string> {
|
|
32
30
|
const env = {
|
|
33
31
|
build: deps.slug,
|
|
@@ -48,7 +46,7 @@ export async function boundedSummary(
|
|
|
48
46
|
summaryMarker(deps.slug),
|
|
49
47
|
`## Autobuild: ${html(deps.slug)}`,
|
|
50
48
|
'',
|
|
51
|
-
`The full summary exceeds the comment limit. It includes ${currentPrAttachments(events).length} PR attachments.`,
|
|
49
|
+
`The full summary exceeds the comment limit. It includes ${(Array.isArray(events) ? currentPrAttachments(events) : (events as CurrentBuildFacts).designations).length} PR attachments.`,
|
|
52
50
|
`Store: <code>${html(deps.storeRef)}</code>`,
|
|
53
51
|
'',
|
|
54
52
|
'Download the complete summary, including all pinned artifact commands and hosted links:',
|
|
@@ -65,26 +63,16 @@ export async function publishSummary(
|
|
|
65
63
|
boundary: { occurrence: number; boundary: number },
|
|
66
64
|
): Promise<'attempted' | 'skipped' | 'deferred'> {
|
|
67
65
|
try {
|
|
68
|
-
const events = await deps.store
|
|
69
|
-
const pr =
|
|
66
|
+
const events = (await readBuildFacts(deps.store, deps.slug)).value
|
|
67
|
+
const pr = events.state.pr
|
|
70
68
|
if (pr === undefined) return 'skipped'
|
|
69
|
+
if (events.summaryAttempts.includes(boundary.occurrence)) return 'attempted'
|
|
71
70
|
if (
|
|
72
|
-
events.
|
|
73
|
-
|
|
74
|
-
event.type === 'pr-summary.attempted' && event.payload.occurrence === boundary.occurrence,
|
|
75
|
-
)
|
|
76
|
-
)
|
|
77
|
-
return 'attempted'
|
|
78
|
-
if (
|
|
79
|
-
events.some(
|
|
80
|
-
(event) =>
|
|
81
|
-
event.type === 'pr-summary.result' &&
|
|
82
|
-
event.payload.eligibleAfter !== undefined &&
|
|
83
|
-
Date.parse(event.payload.eligibleAfter) > deps.clock().getTime(),
|
|
84
|
-
)
|
|
71
|
+
events.summaryEligibleAfter !== undefined &&
|
|
72
|
+
Date.parse(events.summaryEligibleAfter) > deps.clock().getTime()
|
|
85
73
|
)
|
|
86
74
|
return 'deferred'
|
|
87
|
-
const claim = await deps.store.appendIfCurrent(deps.slug, events.
|
|
75
|
+
const claim = await deps.store.appendIfCurrent(deps.slug, events.state.lastSeq, {
|
|
88
76
|
actor: KERNEL,
|
|
89
77
|
type: 'pr-summary.attempted',
|
|
90
78
|
payload: { ...boundary, pr: pr.number },
|
|
@@ -93,7 +81,7 @@ export async function publishSummary(
|
|
|
93
81
|
let outcome: 'success' | 'failure' | 'rate-limited' = 'success'
|
|
94
82
|
let eligibleAfter: string | undefined
|
|
95
83
|
try {
|
|
96
|
-
const body = await boundedSummary(deps, await deps.store
|
|
84
|
+
const body = await boundedSummary(deps, (await readBuildFacts(deps.store, deps.slug)).value)
|
|
97
85
|
await deps.forge.commentOnPr(deps.workspacePath, pr.number, body)
|
|
98
86
|
} catch (error) {
|
|
99
87
|
eligibleAfter = githubRateLimitUntil(error, deps.clock().getTime())
|
|
@@ -118,45 +106,24 @@ export async function publishSummary(
|
|
|
118
106
|
* prevent pipeline progress, including a failed request deposit. */
|
|
119
107
|
export async function refreshSummaryBoundary(deps: SummaryDeps): Promise<boolean> {
|
|
120
108
|
try {
|
|
121
|
-
const events = await deps.store
|
|
122
|
-
const boundary = summaryBoundary
|
|
109
|
+
const events = (await readBuildFacts(deps.store, deps.slug)).value
|
|
110
|
+
const boundary = events.summaryBoundary
|
|
123
111
|
if (boundary === undefined) return false
|
|
124
|
-
const workspace = events.
|
|
112
|
+
const workspace = events.latestWorkspace
|
|
125
113
|
if (workspace?.type === 'workspace.provisioned' && isRemoteWorkspace(workspace.payload)) {
|
|
126
|
-
const request = events.
|
|
127
|
-
(event) =>
|
|
128
|
-
event.type === 'pr-summary.requested' && event.payload.occurrence === boundary.occurrence,
|
|
129
|
-
)
|
|
114
|
+
const request = events.summaryRequested[String(boundary.occurrence)]
|
|
130
115
|
if (request !== undefined) {
|
|
131
116
|
// A lost result/disposition append must not repeatedly park a guest.
|
|
117
|
+
if (events.summaryAttempts.includes(boundary.occurrence)) return false
|
|
132
118
|
if (
|
|
133
|
-
events.
|
|
134
|
-
|
|
135
|
-
event.type === 'pr-summary.attempted' &&
|
|
136
|
-
event.payload.occurrence === boundary.occurrence,
|
|
137
|
-
)
|
|
138
|
-
)
|
|
139
|
-
return false
|
|
140
|
-
if (
|
|
141
|
-
events.some(
|
|
142
|
-
(event) =>
|
|
143
|
-
event.type === 'pr-summary.result' &&
|
|
144
|
-
event.payload.eligibleAfter !== undefined &&
|
|
145
|
-
Date.parse(event.payload.eligibleAfter) > deps.clock().getTime(),
|
|
146
|
-
)
|
|
119
|
+
events.summaryEligibleAfter !== undefined &&
|
|
120
|
+
Date.parse(events.summaryEligibleAfter) > deps.clock().getTime()
|
|
147
121
|
)
|
|
148
122
|
return false
|
|
149
|
-
return
|
|
123
|
+
return events.summaryRequests.some((event) => event.seq === request.seq)
|
|
150
124
|
}
|
|
151
|
-
if (
|
|
152
|
-
|
|
153
|
-
!events.some(
|
|
154
|
-
(event) =>
|
|
155
|
-
event.type === 'publication.requested' && event.payload.operation === 'finalize',
|
|
156
|
-
)
|
|
157
|
-
)
|
|
158
|
-
return false
|
|
159
|
-
const written = await deps.store.appendIfCurrent(deps.slug, events.at(-1)?.seq ?? 0, {
|
|
125
|
+
if (events.state.pr === undefined && !events.finalizePublicationRequested) return false
|
|
126
|
+
const written = await deps.store.appendIfCurrent(deps.slug, events.state.lastSeq, {
|
|
160
127
|
actor: KERNEL,
|
|
161
128
|
type: 'pr-summary.requested',
|
|
162
129
|
payload: boundary,
|
|
@@ -178,10 +145,10 @@ export async function settleSummaryRequests(
|
|
|
178
145
|
let acquired = false
|
|
179
146
|
let heartbeat: ReturnType<typeof setInterval> | undefined
|
|
180
147
|
try {
|
|
181
|
-
let events = await deps.store
|
|
182
|
-
if (
|
|
148
|
+
let events = (await readBuildFacts(deps.store, deps.slug)).value
|
|
149
|
+
if (events.open !== null) return
|
|
183
150
|
// Repair a crash after terminal/session completion but before guest handoff.
|
|
184
|
-
const workspace = events.
|
|
151
|
+
const workspace = events.latestWorkspace
|
|
185
152
|
if (workspace?.type !== 'workspace.provisioned' || !isRemoteWorkspace(workspace.payload)) return
|
|
186
153
|
const holder = deps.reservation ?? deps.holder
|
|
187
154
|
acquired = await deps.store.claimLease(deps.slug, holder, 60_000)
|
|
@@ -189,11 +156,11 @@ export async function settleSummaryRequests(
|
|
|
189
156
|
heartbeat = setInterval(() => {
|
|
190
157
|
void deps.store.heartbeat(deps.slug, holder).catch(() => {})
|
|
191
158
|
}, 15_000)
|
|
192
|
-
events = await deps.store
|
|
193
|
-
if (
|
|
159
|
+
events = (await readBuildFacts(deps.store, deps.slug)).value
|
|
160
|
+
if (events.open !== null) return
|
|
194
161
|
await refreshSummaryBoundary(deps)
|
|
195
|
-
events = await deps.store
|
|
196
|
-
const requests =
|
|
162
|
+
events = (await readBuildFacts(deps.store, deps.slug)).value
|
|
163
|
+
const requests = events.summaryRequests
|
|
197
164
|
const latest = requests.at(-1)
|
|
198
165
|
for (const request of requests) {
|
|
199
166
|
if (request.type !== 'pr-summary.requested') continue
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CurrentBuildFacts } from '../store/reduced-reads'
|
|
1
2
|
import { summaryMarker } from '../ports/forge/summary-marker'
|
|
2
3
|
import type { AbEvent } from '../events/catalog'
|
|
3
4
|
import {
|
|
@@ -33,12 +34,21 @@ export function shellQuote(value: string): string {
|
|
|
33
34
|
return `'${value.replaceAll("'", `'"'"'`)}'`
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
export function renderPrAttachmentSection(
|
|
37
|
-
|
|
37
|
+
export function renderPrAttachmentSection(
|
|
38
|
+
env: CliEnv,
|
|
39
|
+
events: readonly AbEvent[] | CurrentBuildFacts,
|
|
40
|
+
): string[] {
|
|
41
|
+
const designations = Array.isArray(events)
|
|
42
|
+
? currentPrAttachments(events)
|
|
43
|
+
: (events as CurrentBuildFacts).designations
|
|
38
44
|
if (designations.length === 0) return []
|
|
39
45
|
|
|
40
|
-
const target =
|
|
41
|
-
|
|
46
|
+
const target = Array.isArray(events)
|
|
47
|
+
? frozenPrImageHost(events)
|
|
48
|
+
: (events as CurrentBuildFacts).imageHost
|
|
49
|
+
const hosted = Array.isArray(events)
|
|
50
|
+
? hostedPrAttachments(events, designations)
|
|
51
|
+
: new Map((events as CurrentBuildFacts).hostedImages)
|
|
42
52
|
const lines = [
|
|
43
53
|
'',
|
|
44
54
|
'### PR attachments',
|
|
@@ -81,19 +91,39 @@ export function renderPrAttachmentSection(env: CliEnv, events: readonly AbEvent[
|
|
|
81
91
|
}
|
|
82
92
|
|
|
83
93
|
/** The complete PR summary projection, rendered only from durable facts. */
|
|
84
|
-
export function renderPrSummary(
|
|
94
|
+
export function renderPrSummary(
|
|
95
|
+
env: CliEnv,
|
|
96
|
+
events: readonly AbEvent[] | CurrentBuildFacts,
|
|
97
|
+
): string {
|
|
85
98
|
const verdicts: string[] = []
|
|
86
99
|
const verifies: string[] = []
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
const history = Array.isArray(events)
|
|
101
|
+
? events.flatMap((event): CurrentBuildFacts['summaryHistory'] =>
|
|
102
|
+
event.type === 'plan-review.verdict' || event.type === 'code-review.verdict'
|
|
103
|
+
? [
|
|
104
|
+
{
|
|
105
|
+
kind: 'review',
|
|
106
|
+
phase: event.type === 'plan-review.verdict' ? 'plan-review' : 'code-review',
|
|
107
|
+
round: event.payload.round,
|
|
108
|
+
verdict: event.payload.verdict,
|
|
109
|
+
count: event.payload.findings.length,
|
|
110
|
+
},
|
|
111
|
+
]
|
|
112
|
+
: event.type === 'verify.completed'
|
|
113
|
+
? [{ kind: 'verify', result: event.payload }]
|
|
114
|
+
: [],
|
|
115
|
+
)
|
|
116
|
+
: (events as CurrentBuildFacts).summaryHistory
|
|
117
|
+
for (const entry of history) {
|
|
118
|
+
if (entry.kind === 'review') {
|
|
119
|
+
const phase = entry.phase
|
|
120
|
+
const count = entry.count
|
|
91
121
|
const detail =
|
|
92
|
-
|
|
93
|
-
verdicts.push(`- ${phase} r${
|
|
122
|
+
entry.verdict === 'revise' ? ` (${count} finding${count === 1 ? '' : 's'})` : ''
|
|
123
|
+
verdicts.push(`- ${phase} r${entry.round}: ${entry.verdict}${detail}`)
|
|
94
124
|
}
|
|
95
|
-
if (
|
|
96
|
-
const result = normalizeVerifyCompletion(
|
|
125
|
+
if (entry.kind === 'verify' && entry.result) {
|
|
126
|
+
const result = normalizeVerifyCompletion(entry.result)
|
|
97
127
|
const detail =
|
|
98
128
|
result.outcome === 'skipped'
|
|
99
129
|
? ` — ${html(result.reason ?? '')}`
|