@defrex/autobuild 0.8.0 → 0.10.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 +323 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +166 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +175 -28
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +204 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +14 -0
- package/package.json +45 -23
- package/packages/core/src/cli/artifact.ts +130 -6
- package/packages/core/src/cli/binary.ts +9 -1
- package/packages/core/src/cli/build-control.ts +110 -13
- package/packages/core/src/cli/context.ts +46 -0
- package/packages/core/src/cli/dashboard/facts.ts +215 -0
- package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
- package/packages/core/src/cli/dashboard/model.ts +141 -153
- package/packages/core/src/cli/dashboard/poll.ts +76 -12
- package/packages/core/src/cli/dashboard/render.ts +34 -5
- package/packages/core/src/cli/dispatch-frontend.ts +54 -5
- package/packages/core/src/cli/dispatch-process.ts +1 -13
- package/packages/core/src/cli/dispatch.ts +392 -75
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +62 -9
- package/packages/core/src/cli/init-readiness-remote.ts +110 -0
- package/packages/core/src/cli/init-readiness-shared.ts +72 -0
- package/packages/core/src/cli/init-validation.ts +266 -222
- package/packages/core/src/cli/installation.ts +50 -8
- package/packages/core/src/cli/main.ts +70 -11
- package/packages/core/src/cli/mcp.ts +23 -1
- package/packages/core/src/cli/plugin.ts +7 -4
- package/packages/core/src/cli/remote-poll.ts +277 -0
- package/packages/core/src/cli/repo-state.ts +22 -0
- package/packages/core/src/cli/repository-status.ts +39 -1
- package/packages/core/src/cli/self-update.ts +77 -2
- package/packages/core/src/cli/status.ts +150 -28
- package/packages/core/src/cli/terminals.ts +2 -1
- package/packages/core/src/cli/ticket-assets.ts +247 -0
- package/packages/core/src/cli/ticket.ts +453 -8
- package/packages/core/src/cli/wait.ts +157 -41
- package/packages/core/src/cli/watch.ts +138 -168
- package/packages/core/src/config/live.ts +23 -0
- package/packages/core/src/config/pipeline-source.ts +388 -0
- package/packages/core/src/config/schema.ts +251 -19
- package/packages/core/src/events/catalog.ts +1 -0
- package/packages/core/src/events/globs.ts +47 -0
- package/packages/core/src/events/payloads.ts +52 -2
- package/packages/core/src/events/repository.ts +52 -1
- package/packages/core/src/events/sessions.ts +69 -10
- package/packages/core/src/events/workspace-remote.ts +12 -0
- package/packages/core/src/integration/harness.ts +115 -2
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
- package/packages/core/src/kernel/auto-merge-default.ts +157 -0
- package/packages/core/src/kernel/auto-merge.ts +99 -22
- package/packages/core/src/kernel/dispatch-settings.ts +42 -17
- package/packages/core/src/kernel/dispatch-status.ts +23 -0
- package/packages/core/src/kernel/engine.ts +9 -438
- package/packages/core/src/kernel/generators/build-log.ts +259 -0
- package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
- package/packages/core/src/kernel/generators/session-log.ts +72 -0
- package/packages/core/src/kernel/harvest.ts +62 -18
- package/packages/core/src/kernel/incremental-contract.ts +75 -0
- package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
- package/packages/core/src/kernel/incremental.ts +53 -0
- package/packages/core/src/kernel/log-index.ts +620 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/pr-attachments.ts +164 -46
- package/packages/core/src/kernel/reducer-registry.ts +164 -0
- package/packages/core/src/kernel/reducer.ts +254 -73
- package/packages/core/src/operator/annotations.ts +6 -0
- package/packages/core/src/operator/control.ts +19 -5
- package/packages/core/src/operator/index.ts +71 -6
- package/packages/core/src/operator/query.ts +253 -24
- package/packages/core/src/operator/read-view.ts +98 -0
- package/packages/core/src/operator/registry.ts +55 -10
- package/packages/core/src/operator/requests.ts +9 -6
- package/packages/core/src/operator/sandbox-host.ts +140 -0
- package/packages/core/src/operator/sandbox.ts +247 -14
- package/packages/core/src/operator/ticket-source-backend.ts +47 -0
- package/packages/core/src/orchestrator/approvals.ts +115 -0
- package/packages/core/src/orchestrator/conversation.ts +167 -0
- package/packages/core/src/orchestrator/model.ts +86 -0
- package/packages/core/src/orchestrator/prompt.ts +61 -0
- package/packages/core/src/orchestrator/tools.ts +117 -0
- package/packages/core/src/orchestrator/turn-runner.ts +457 -0
- package/packages/core/src/plugin-sdk/index.ts +51 -1
- package/packages/core/src/plugins/load.ts +135 -7
- package/packages/core/src/plugins/manifest.ts +183 -10
- package/packages/core/src/plugins/registry.ts +73 -4
- package/packages/core/src/ports/forge/create.ts +1 -1
- package/packages/core/src/ports/forge/fake.ts +31 -0
- package/packages/core/src/ports/forge/github-transport.ts +13 -2
- package/packages/core/src/ports/forge/github.ts +13 -2
- package/packages/core/src/ports/forge/local-git.ts +31 -1
- package/packages/core/src/ports/runner/claude.ts +2 -2
- package/packages/core/src/ports/runner/codex.ts +6 -2
- package/packages/core/src/ports/runner/fake.ts +7 -1
- package/packages/core/src/ports/runner/pi.ts +8 -6
- package/packages/core/src/ports/runner/provider-error.ts +13 -3
- package/packages/core/src/ports/tickets/fake.ts +5 -1
- package/packages/core/src/ports/tickets/file.ts +169 -48
- package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/tickets/linear.ts +33 -1
- package/packages/core/src/ports/tickets/operations.ts +1 -1
- package/packages/core/src/ports/tickets/remote.ts +8 -1
- package/packages/core/src/ports/types.ts +49 -3
- package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
- package/packages/core/src/ports/workspace/create.ts +74 -39
- package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
- package/packages/core/src/ports/workspace/fake.ts +169 -4
- package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
- package/packages/core/src/ports/workspace/github-origin.ts +36 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
- package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
- package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
- package/packages/core/src/processes/build-child.ts +17 -2
- package/packages/core/src/processes/build-execution-state.ts +68 -15
- package/packages/core/src/processes/build-runner.ts +238 -93
- package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
- package/packages/core/src/processes/dispatcher.ts +501 -201
- package/packages/core/src/processes/execution-settlement.ts +72 -39
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-execution-state.ts +41 -29
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
- package/packages/core/src/processes/orchestrator-tick.ts +360 -0
- package/packages/core/src/processes/publication-state.ts +164 -52
- package/packages/core/src/processes/repo-view.ts +1436 -0
- package/packages/core/src/processes/sandbox-state.ts +36 -2
- package/packages/core/src/processes/tick-state.ts +33 -0
- package/packages/core/src/store/adapter.ts +41 -0
- package/packages/core/src/store/build-scope.ts +90 -0
- package/packages/core/src/store/contract.ts +1205 -0
- package/packages/core/src/store/digest.ts +102 -0
- package/packages/core/src/store/local/schema.ts +46 -2
- package/packages/core/src/store/local/store.ts +445 -4
- package/packages/core/src/store/memory.ts +246 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +94 -0
- package/packages/core/src/store/remote/client.ts +310 -2
- package/packages/core/src/store/remote/index.ts +14 -2
- package/packages/core/src/store/remote/protocol.ts +115 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +205 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +94 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
- package/packages/core/src/store/snapshots.ts +88 -0
- package/packages/core/src/store/streams/session-writer.ts +42 -2
- package/packages/core/src/store/streams/wait.ts +4 -5
- package/packages/core/src/store/ticket-assets.ts +458 -0
- package/packages/core/src/store/types.ts +160 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +152 -40
- package/skills/guide/references/operator-api.md +2 -2
- package/skills/guide/references/plugin-authoring.md +17 -0
- package/skills/guide/references/remote-store-protocol.md +204 -16
- package/skills/guide/references/remote-ticket-protocol.md +1 -1
- package/skills/guide/references/setup.md +12 -9
- package/skills/implement/SKILL.md +3 -0
- package/skills/operate/SKILL.md +126 -0
- package/skills/plan/SKILL.md +4 -0
- package/skills/plan-review/SKILL.md +5 -1
- package/skills/spec/SKILL.md +46 -3
- package/skills/tickets/SKILL.md +57 -5
- package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
- package/packages/core/src/cli/store-opening.contract.ts +0 -289
- package/packages/core/src/cli/testkit.ts +0 -194
- package/packages/core/src/markdown.ts +0 -67
- package/packages/core/src/operator/client.ts +0 -311
- package/packages/core/src/operator/server.ts +0 -616
- package/packages/core/src/operator/sessions.ts +0 -223
- package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
- package/packages/core/src/ports/tickets/remote-server.ts +0 -185
- package/packages/core/src/store/remote/server.ts +0 -923
- package/packages/core/src/testing/store-failures.ts +0 -32
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test support for the incremental reducer contract. Imported only by tests.
|
|
3
|
+
*/
|
|
4
|
+
import { expect } from 'bun:test'
|
|
5
|
+
import type { IncrementalReducer } from './incremental'
|
|
6
|
+
|
|
7
|
+
export interface CheckOptions<State> {
|
|
8
|
+
/** Equality for the oracle comparison. Defaults to `toEqual`, which treats
|
|
9
|
+
* `undefined` and missing keys alike (the JSON round trip needs that). */
|
|
10
|
+
compare?: (actual: State, expected: State) => void
|
|
11
|
+
/** Also fold one event at a time. Default true. */
|
|
12
|
+
stepwise?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const roundTrip = <T>(value: T): T => JSON.parse(JSON.stringify(value)) as T
|
|
16
|
+
|
|
17
|
+
/** Assert that for every split point `k`, advancing the accumulator of
|
|
18
|
+
* `log[0..k]` with `log[k..]` equals reducing the whole log — in memory and
|
|
19
|
+
* through a JSON round trip — and that `advance` never mutates its input. */
|
|
20
|
+
export function checkIncremental<Acc, Event, State>(
|
|
21
|
+
reducer: IncrementalReducer<Acc, Event, State>,
|
|
22
|
+
log: readonly Event[],
|
|
23
|
+
options: CheckOptions<State> = {},
|
|
24
|
+
): void {
|
|
25
|
+
const compare = options.compare ?? ((a, b) => expect(a).toEqual(b))
|
|
26
|
+
const oracle = reducer.reduce(log)
|
|
27
|
+
for (let k = 0; k <= log.length; k++) {
|
|
28
|
+
const prefix = reducer.advance(reducer.initial(), log.slice(0, k))
|
|
29
|
+
const before = structuredClone(prefix)
|
|
30
|
+
const earlier = reducer.finish(prefix)
|
|
31
|
+
const earlierSnapshot = structuredClone(earlier)
|
|
32
|
+
const suffix = log.slice(k)
|
|
33
|
+
|
|
34
|
+
const advanced = reducer.advance(prefix, suffix)
|
|
35
|
+
expect(prefix).toEqual(before)
|
|
36
|
+
compare(reducer.finish(advanced), oracle)
|
|
37
|
+
// Advancing later must not disturb a state derived earlier.
|
|
38
|
+
expect(earlier).toEqual(earlierSnapshot)
|
|
39
|
+
|
|
40
|
+
const revived = reducer.advance(roundTrip(prefix), suffix)
|
|
41
|
+
compare(reducer.finish(revived), oracle)
|
|
42
|
+
// A serialized finished accumulator must also revive to the same state.
|
|
43
|
+
compare(reducer.finish(roundTrip(advanced)), oracle)
|
|
44
|
+
}
|
|
45
|
+
if (options.stepwise ?? true) {
|
|
46
|
+
let acc = reducer.initial()
|
|
47
|
+
for (const event of log) acc = reducer.advance(acc, [event])
|
|
48
|
+
compare(reducer.finish(acc), oracle)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A small seeded PRNG (mulberry32) so generated logs reproduce from a seed. */
|
|
53
|
+
export function seededRandom(seed: number): () => number {
|
|
54
|
+
let a = seed >>> 0
|
|
55
|
+
return () => {
|
|
56
|
+
a = (a + 0x6d2b79f5) >>> 0
|
|
57
|
+
let t = a
|
|
58
|
+
t = Math.imul(t ^ (t >>> 15), t | 1)
|
|
59
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
|
|
60
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function pick<T>(rand: () => number, items: readonly T[]): T {
|
|
65
|
+
return items[Math.floor(rand() * items.length)] as T
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function shuffled<T>(rand: () => number, items: readonly T[]): T[] {
|
|
69
|
+
const out = [...items]
|
|
70
|
+
for (let i = out.length - 1; i > 0; i--) {
|
|
71
|
+
const j = Math.floor(rand() * (i + 1))
|
|
72
|
+
;[out[i], out[j]] = [out[j] as T, out[i] as T]
|
|
73
|
+
}
|
|
74
|
+
return out
|
|
75
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test support for the incremental-reducer property tests of the small
|
|
3
|
+
* selector projections: build and repository event generators drawn from
|
|
4
|
+
* small value pools (so correlations happen), seeded, and shuffled to prove
|
|
5
|
+
* the carried state reproduces the whole-array result for any array order.
|
|
6
|
+
* Imported only by tests. Events are cast, not validated: the projections
|
|
7
|
+
* under test read only the fields the generators set.
|
|
8
|
+
*/
|
|
9
|
+
import type { AbEvent } from '../events/catalog'
|
|
10
|
+
import { KERNEL } from '../events/envelope'
|
|
11
|
+
import type { RepositoryEvent } from '../events/repository'
|
|
12
|
+
import { expect } from 'bun:test'
|
|
13
|
+
import { checkIncremental, pick, seededRandom, shuffled } from './incremental-contract'
|
|
14
|
+
import type { IncrementalReducer } from './incremental'
|
|
15
|
+
|
|
16
|
+
export type Rand = () => number
|
|
17
|
+
|
|
18
|
+
export function buildLog(
|
|
19
|
+
items: ReadonlyArray<readonly [string, Record<string, unknown>]>,
|
|
20
|
+
): AbEvent[] {
|
|
21
|
+
return items.map(
|
|
22
|
+
([type, payload], index) =>
|
|
23
|
+
({
|
|
24
|
+
build: 'b',
|
|
25
|
+
seq: index + 1,
|
|
26
|
+
ts: new Date(Date.UTC(2026, 0, 1, 0, 0, index)).toISOString(),
|
|
27
|
+
actor: KERNEL,
|
|
28
|
+
type,
|
|
29
|
+
payload,
|
|
30
|
+
}) as unknown as AbEvent,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function repoLog(
|
|
35
|
+
items: ReadonlyArray<readonly [string, Record<string, unknown>]>,
|
|
36
|
+
): RepositoryEvent[] {
|
|
37
|
+
return items.map(
|
|
38
|
+
([type, payload], index) =>
|
|
39
|
+
({
|
|
40
|
+
repo: 'r',
|
|
41
|
+
seq: index + 1,
|
|
42
|
+
ts: new Date(Date.UTC(2026, 0, 1, 0, 0, index)).toISOString(),
|
|
43
|
+
actor: KERNEL,
|
|
44
|
+
type,
|
|
45
|
+
payload,
|
|
46
|
+
}) as unknown as RepositoryEvent,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** For each seed: an ordered log and two shuffled copies of it (seq is
|
|
51
|
+
* assigned before shuffling, so shuffled arrays carry out-of-order seqs). */
|
|
52
|
+
export function generatedLogs<E>(
|
|
53
|
+
make: (items: ReadonlyArray<readonly [string, Record<string, unknown>]>) => E[],
|
|
54
|
+
item: (rand: Rand) => readonly [string, Record<string, unknown>],
|
|
55
|
+
seeds: readonly number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
|
|
56
|
+
): E[][] {
|
|
57
|
+
const out: E[][] = []
|
|
58
|
+
for (const seed of seeds) {
|
|
59
|
+
const rand = seededRandom(seed)
|
|
60
|
+
const length = 4 + Math.floor(rand() * 16)
|
|
61
|
+
const ordered = make(Array.from({ length }, () => item(rand)))
|
|
62
|
+
out.push(ordered, shuffled(rand, ordered), shuffled(rand, ordered))
|
|
63
|
+
}
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A random build event of the kinds the process-layer selectors read. */
|
|
68
|
+
export function randomBuildItem(rand: Rand): readonly [string, Record<string, unknown>] {
|
|
69
|
+
const instance = pick(rand, ['i1', 'i2'])
|
|
70
|
+
const attempt = 1 + Math.floor(rand() * 3)
|
|
71
|
+
const id = pick(rand, ['e1', 'e2', 'e3'])
|
|
72
|
+
const sha = pick(rand, ['s1', 's2', 's3', 's4'])
|
|
73
|
+
const ref = pick(rand, ['/ws/a', '/ws/b'])
|
|
74
|
+
const marker = pick(rand, [
|
|
75
|
+
'auto-merge-gate:pr:7:command:1',
|
|
76
|
+
'auto-merge-gate:pr:7:command:2',
|
|
77
|
+
'other',
|
|
78
|
+
])
|
|
79
|
+
const assets = () =>
|
|
80
|
+
rand() < 0.3
|
|
81
|
+
? undefined
|
|
82
|
+
: [
|
|
83
|
+
{ kind: 'doc', name: pick(rand, ['a', 'b']), revision: 1 + Math.floor(rand() * 3) },
|
|
84
|
+
...(rand() < 0.5 ? [{ kind: 'img', name: 'c', revision: 1 }] : []),
|
|
85
|
+
]
|
|
86
|
+
const templates: Array<() => readonly [string, Record<string, unknown>]> = [
|
|
87
|
+
() => ['build.created', { baseBranch: pick(rand, ['main', 'dev']), assets: assets() }],
|
|
88
|
+
() => ['spec.revised', { artifact: { kind: 'spec', rev: 1 }, escalation: 1, assets: assets() }],
|
|
89
|
+
() => [
|
|
90
|
+
'execution.started',
|
|
91
|
+
{
|
|
92
|
+
provider: 'p',
|
|
93
|
+
workspaceRef: ref,
|
|
94
|
+
instance,
|
|
95
|
+
...(rand() < 0.5 ? { environmentId: 'env' } : {}),
|
|
96
|
+
...(rand() < 0.5 ? { sessionId: 'sess' } : {}),
|
|
97
|
+
...(rand() < 0.5 ? { commandId: 'cmd' } : {}),
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
() => [
|
|
101
|
+
'execution.ended',
|
|
102
|
+
{ instance, workspaceRef: ref, outcome: pick(rand, ['completed', 'stopped', 'lost']) },
|
|
103
|
+
],
|
|
104
|
+
() => [
|
|
105
|
+
'workspace.provisioned',
|
|
106
|
+
{
|
|
107
|
+
provider: 'p',
|
|
108
|
+
ref,
|
|
109
|
+
...(rand() < 0.5 ? { path: '/p' } : {}),
|
|
110
|
+
branch: 'ab/x',
|
|
111
|
+
base: { source: 'remote', sha },
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
() => ['workspace.released', {}],
|
|
115
|
+
() => ['workspace.provision-started', { provider: 'p', branch: 'ab/x', generation: attempt }],
|
|
116
|
+
() => ['implement.completed', { round: 1, commits: { base: 'b', head: sha }, artifact: {} }],
|
|
117
|
+
() => ['reconcile.started', { attempt, baseSha: sha }],
|
|
118
|
+
() => ['reconcile.completed', { mergeCommit: sha, artifact: {} }],
|
|
119
|
+
() => [
|
|
120
|
+
'finalize.step-completed',
|
|
121
|
+
{ step: 'n', ok: rand() < 0.7, ...(rand() < 0.7 ? { headSha: sha } : {}) },
|
|
122
|
+
],
|
|
123
|
+
() => ['finalize.completed', { pr: { number: 7, url: 'u', headSha: sha } }],
|
|
124
|
+
() => ['pr.merged', { sha }],
|
|
125
|
+
() => ['pr.closed', {}],
|
|
126
|
+
() => ['runner.attached', { instance, host: 'h' }],
|
|
127
|
+
() => ['runner.setup-failed', { command: 'c', attempt, exitStatus: 1, output: '' }],
|
|
128
|
+
() => [
|
|
129
|
+
'escalation.raised',
|
|
130
|
+
{
|
|
131
|
+
id,
|
|
132
|
+
phase: pick(rand, ['setup', 'plan', 'implement', 'verify:unit']),
|
|
133
|
+
source: pick(rand, ['policy', 'agent']),
|
|
134
|
+
...(rand() < 0.7
|
|
135
|
+
? { policyCause: pick(rand, ['infrastructure-failure-limit', 'setup-failure-limit']) }
|
|
136
|
+
: {}),
|
|
137
|
+
...(rand() < 0.5 ? { round: 1 } : {}),
|
|
138
|
+
question: 'q',
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
() => [
|
|
142
|
+
'escalation.answered',
|
|
143
|
+
{ id, answer: 'a', resolution: pick(rand, ['retry', 'guidance', 'abort']) },
|
|
144
|
+
],
|
|
145
|
+
() => [
|
|
146
|
+
'phase.failed',
|
|
147
|
+
{
|
|
148
|
+
phase: pick(rand, ['plan', 'implement']),
|
|
149
|
+
round: 1 + Math.floor(rand() * 2),
|
|
150
|
+
attempt,
|
|
151
|
+
error: `err${attempt}`,
|
|
152
|
+
willRetry: rand() < 0.5,
|
|
153
|
+
...(rand() < 0.5 ? { providerAttempts: [{ index: 1, runner: 'r', error: 'e' }] } : {}),
|
|
154
|
+
},
|
|
155
|
+
],
|
|
156
|
+
() => ['observation.recorded', { id: 'o', kind: 'followup', summary: 's', refs: [marker] }],
|
|
157
|
+
() => ['observation.recorded', { id: 'o2', kind: 'refactor', summary: 's' }],
|
|
158
|
+
() => ['session.started', { session: 's', role: 'r', runner: 'c', phase: 'plan' }],
|
|
159
|
+
]
|
|
160
|
+
return pick(rand, templates)()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** A random repository-journal event of the kinds the repo selectors read. */
|
|
164
|
+
export function randomRepoItem(rand: Rand): readonly [string, Record<string, unknown>] {
|
|
165
|
+
const execution = pick(rand, ['x1', 'x2', 'x3'])
|
|
166
|
+
const run = pick(rand, ['r1', 'r2'])
|
|
167
|
+
const templates: Array<() => readonly [string, Record<string, unknown>]> = [
|
|
168
|
+
() => [
|
|
169
|
+
'harvest.execution.started',
|
|
170
|
+
{
|
|
171
|
+
execution,
|
|
172
|
+
provider: 'p',
|
|
173
|
+
environmentId: 'env',
|
|
174
|
+
...(rand() < 0.5 ? { sessionId: 'sess' } : {}),
|
|
175
|
+
...(rand() < 0.5 ? { commandId: 'cmd' } : {}),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
() => [
|
|
179
|
+
'harvest.execution.released',
|
|
180
|
+
{ execution, environmentId: 'env', snapshots: { outcome: 'confirmed' } },
|
|
181
|
+
],
|
|
182
|
+
() => ['dispatcher.tick-started', { run }],
|
|
183
|
+
() => ['dispatcher.tick-completed', { run, queued: 0 }],
|
|
184
|
+
() => ['dispatcher.tick-failed', { run, error: 'x' }],
|
|
185
|
+
() => ['dispatcher.tick-yielded', { holder: 'h' }],
|
|
186
|
+
() => ['dispatcher.operator-reported', { run, level: 'info', message: 'm' }],
|
|
187
|
+
() => ['dispatcher.auto-merge-default-set', { enabled: rand() < 0.5 }],
|
|
188
|
+
() => ['dispatcher.intake-set', { enabled: rand() < 0.5 }],
|
|
189
|
+
]
|
|
190
|
+
return pick(rand, templates)()
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Run the incremental contract over every log, and hold the result to an
|
|
194
|
+
* independent reference implementation of the original whole-array function. */
|
|
195
|
+
export function checkAgainstReference<Acc, Event, State>(
|
|
196
|
+
reducer: IncrementalReducer<Acc, Event, State>,
|
|
197
|
+
logs: readonly (readonly Event[])[],
|
|
198
|
+
reference: (events: readonly Event[]) => State,
|
|
199
|
+
normalize: (state: State) => unknown = (state) => state,
|
|
200
|
+
): void {
|
|
201
|
+
for (const log of logs) {
|
|
202
|
+
const expected = normalize(reference(log))
|
|
203
|
+
expect(normalize(reducer.reduce(log))).toEqual(expected)
|
|
204
|
+
checkIncremental(reducer, log, {
|
|
205
|
+
compare: (actual, oracle) => {
|
|
206
|
+
expect(normalize(actual)).toEqual(normalize(oracle))
|
|
207
|
+
expect(normalize(actual)).toEqual(expected)
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The incremental reducer contract: every projection of an event array is a
|
|
3
|
+
* fold plus a final derivation, so its state can be advanced from a previously
|
|
4
|
+
* computed accumulator using only the events newer than it (CLAUDE.md rule 2 —
|
|
5
|
+
* state is a reduction of the log, and any carried accumulator is a cache).
|
|
6
|
+
*
|
|
7
|
+
* The accumulator is plain JSON (no `Map`, `Set`, or class instances), so it
|
|
8
|
+
* survives `JSON.parse(JSON.stringify(acc))` and can be stored and resumed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface IncrementalReducer<Acc, Event, State> {
|
|
12
|
+
/** Bump when `Acc`'s shape or the fold's semantics change: a cached
|
|
13
|
+
* accumulator is only valid for the version that wrote it. */
|
|
14
|
+
readonly version: number
|
|
15
|
+
/** The accumulator for an empty log. */
|
|
16
|
+
initial(): Acc
|
|
17
|
+
/** Fold events newer than `acc` into a copy of it. Never mutates `acc`. */
|
|
18
|
+
advance(acc: Acc, events: readonly Event[]): Acc
|
|
19
|
+
/** Derive the public state. Returns fresh top-level objects. */
|
|
20
|
+
finish(acc: Acc): State
|
|
21
|
+
/** The whole-array form: `finish(advance(initial(), events))` without the
|
|
22
|
+
* clone. */
|
|
23
|
+
reduce(events: readonly Event[]): State
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function cloneAcc<T>(acc: T): T {
|
|
27
|
+
return structuredClone(acc)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Derive `advance` (clone, then fold in place) and `reduce` from an in-place
|
|
31
|
+
* `fold`. */
|
|
32
|
+
export function defineReducer<Acc, Event, State>(def: {
|
|
33
|
+
version: number
|
|
34
|
+
initial: () => Acc
|
|
35
|
+
fold: (acc: Acc, events: readonly Event[]) => void
|
|
36
|
+
finish: (acc: Acc) => State
|
|
37
|
+
}): IncrementalReducer<Acc, Event, State> {
|
|
38
|
+
return {
|
|
39
|
+
version: def.version,
|
|
40
|
+
initial: def.initial,
|
|
41
|
+
advance(acc, events) {
|
|
42
|
+
const next = cloneAcc(acc)
|
|
43
|
+
def.fold(next, events)
|
|
44
|
+
return next
|
|
45
|
+
},
|
|
46
|
+
finish: def.finish,
|
|
47
|
+
reduce(events) {
|
|
48
|
+
const acc = def.initial()
|
|
49
|
+
def.fold(acc, events)
|
|
50
|
+
return def.finish(acc)
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|