@defrex/autobuild 0.8.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/LICENSE +201 -0
- package/README.md +192 -0
- package/SPEC.md +2269 -0
- package/bin/ab-build-runner.ts +47 -0
- package/bin/ab-dev.ts +27 -0
- package/bin/ab-dispatch-kernel.ts +92 -0
- package/bin/ab-harvest-runner.ts +29 -0
- package/bin/ab-init-probe.ts +19 -0
- package/bin/ab-process-group-reaper.ts +32 -0
- package/bin/ab.ts +6 -0
- package/bin/agent/ab +3 -0
- package/docs/answering-blocked-builds.md +89 -0
- package/docs/architecture.md +726 -0
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +1623 -0
- package/docs/hosted-dispatcher-cutover-validation.md +178 -0
- package/docs/hosted-dispatcher.md +204 -0
- package/docs/mcp.md +119 -0
- package/docs/operator-api.md +186 -0
- package/docs/remote-store-protocol.md +972 -0
- package/docs/remote-ticket-protocol.md +79 -0
- package/docs/setup.md +289 -0
- package/docs/spec-standard.md +63 -0
- package/docs/ticket-dependencies.md +59 -0
- package/docs/vercel-sandbox-rollout-probe.md +7 -0
- package/package.json +135 -0
- package/packages/core/src/cli/args.ts +74 -0
- package/packages/core/src/cli/artifact.ts +182 -0
- package/packages/core/src/cli/binary.ts +198 -0
- package/packages/core/src/cli/build-control.ts +789 -0
- package/packages/core/src/cli/build-progress.ts +55 -0
- package/packages/core/src/cli/bulk-control.ts +385 -0
- package/packages/core/src/cli/context.ts +470 -0
- package/packages/core/src/cli/dashboard/actions.ts +59 -0
- package/packages/core/src/cli/dashboard/cells.ts +92 -0
- package/packages/core/src/cli/dashboard/composer.ts +230 -0
- package/packages/core/src/cli/dashboard/detail.ts +100 -0
- package/packages/core/src/cli/dashboard/frame-image.ts +373 -0
- package/packages/core/src/cli/dashboard/live.ts +152 -0
- package/packages/core/src/cli/dashboard/model.ts +1173 -0
- package/packages/core/src/cli/dashboard/poll.ts +196 -0
- package/packages/core/src/cli/dashboard/render.ts +1568 -0
- package/packages/core/src/cli/dashboard/selection.ts +66 -0
- package/packages/core/src/cli/dashboard/session-feed.ts +244 -0
- package/packages/core/src/cli/dashboard/session-view.ts +506 -0
- package/packages/core/src/cli/dashboard/transcript.ts +108 -0
- package/packages/core/src/cli/dev.ts +46 -0
- package/packages/core/src/cli/dispatch-frontend.ts +1045 -0
- package/packages/core/src/cli/dispatch-process.ts +259 -0
- package/packages/core/src/cli/dispatch.ts +3692 -0
- package/packages/core/src/cli/dotenv.ts +48 -0
- package/packages/core/src/cli/env.ts +206 -0
- package/packages/core/src/cli/harvest.ts +698 -0
- package/packages/core/src/cli/help.ts +713 -0
- package/packages/core/src/cli/init-agent.ts +106 -0
- package/packages/core/src/cli/init-validation.ts +583 -0
- package/packages/core/src/cli/init.ts +756 -0
- package/packages/core/src/cli/installation.ts +333 -0
- package/packages/core/src/cli/keyboard.ts +261 -0
- package/packages/core/src/cli/main.ts +1399 -0
- package/packages/core/src/cli/mcp.ts +279 -0
- package/packages/core/src/cli/models.ts +65 -0
- package/packages/core/src/cli/observe.ts +55 -0
- package/packages/core/src/cli/plugin.ts +227 -0
- package/packages/core/src/cli/pr-attachments.ts +138 -0
- package/packages/core/src/cli/pr-summary.ts +124 -0
- package/packages/core/src/cli/publication-settlement.ts +188 -0
- package/packages/core/src/cli/repo-state.ts +213 -0
- package/packages/core/src/cli/repository-status.ts +80 -0
- package/packages/core/src/cli/self-update.ts +443 -0
- package/packages/core/src/cli/status.ts +895 -0
- package/packages/core/src/cli/store-opening.contract.ts +289 -0
- package/packages/core/src/cli/store-opening.ts +167 -0
- package/packages/core/src/cli/store-ref.ts +33 -0
- package/packages/core/src/cli/terminal-restore.ts +174 -0
- package/packages/core/src/cli/terminal.ts +361 -0
- package/packages/core/src/cli/terminals.ts +995 -0
- package/packages/core/src/cli/testkit.ts +194 -0
- package/packages/core/src/cli/ticket.ts +484 -0
- package/packages/core/src/cli/upgrade-agent.ts +180 -0
- package/packages/core/src/cli/upgrade-commit.ts +468 -0
- package/packages/core/src/cli/upgrade-notice.ts +91 -0
- package/packages/core/src/cli/upgrade-progress.ts +110 -0
- package/packages/core/src/cli/upgrade.ts +888 -0
- package/packages/core/src/cli/wait.ts +587 -0
- package/packages/core/src/cli/watch.ts +932 -0
- package/packages/core/src/config/live.ts +255 -0
- package/packages/core/src/config/load.ts +98 -0
- package/packages/core/src/config/roles.ts +438 -0
- package/packages/core/src/config/schema.ts +774 -0
- package/packages/core/src/distribution.ts +24 -0
- package/packages/core/src/events/catalog.ts +201 -0
- package/packages/core/src/events/envelope.ts +59 -0
- package/packages/core/src/events/payloads.ts +618 -0
- package/packages/core/src/events/repository.ts +559 -0
- package/packages/core/src/events/sessions.ts +180 -0
- package/packages/core/src/harvest/schema.ts +147 -0
- package/packages/core/src/ids.ts +45 -0
- package/packages/core/src/integration/harness.ts +681 -0
- package/packages/core/src/kernel/auto-merge.ts +259 -0
- package/packages/core/src/kernel/converge.ts +117 -0
- package/packages/core/src/kernel/dispatch-settings.ts +60 -0
- package/packages/core/src/kernel/dispatch-status.ts +141 -0
- package/packages/core/src/kernel/engine.ts +1047 -0
- package/packages/core/src/kernel/harvest.ts +823 -0
- package/packages/core/src/kernel/origin.ts +69 -0
- package/packages/core/src/kernel/phases.ts +196 -0
- package/packages/core/src/kernel/plan-verify-selection.ts +105 -0
- package/packages/core/src/kernel/pr-attachments.ts +82 -0
- package/packages/core/src/kernel/reducer.ts +752 -0
- package/packages/core/src/kernel/stall.ts +155 -0
- package/packages/core/src/kernel/verify-applicability.ts +52 -0
- package/packages/core/src/markdown.ts +67 -0
- package/packages/core/src/ontology.ts +288 -0
- package/packages/core/src/open-map.ts +203 -0
- package/packages/core/src/operator/annotations.ts +187 -0
- package/packages/core/src/operator/client.ts +311 -0
- package/packages/core/src/operator/control.ts +117 -0
- package/packages/core/src/operator/index.ts +62 -0
- package/packages/core/src/operator/presentation.ts +22 -0
- package/packages/core/src/operator/protocol.ts +98 -0
- package/packages/core/src/operator/query.ts +201 -0
- package/packages/core/src/operator/registry.ts +1043 -0
- package/packages/core/src/operator/requests.ts +138 -0
- package/packages/core/src/operator/sandbox.ts +458 -0
- package/packages/core/src/operator/server.ts +616 -0
- package/packages/core/src/operator/sessions.ts +223 -0
- package/packages/core/src/operator/tickets.ts +177 -0
- package/packages/core/src/plugin-sdk/index.ts +174 -0
- package/packages/core/src/plugins/contract-entry.ts +74 -0
- package/packages/core/src/plugins/contract-env.ts +3 -0
- package/packages/core/src/plugins/load.ts +259 -0
- package/packages/core/src/plugins/manifest.ts +236 -0
- package/packages/core/src/plugins/registry.ts +274 -0
- package/packages/core/src/plugins/runtimes.ts +75 -0
- package/packages/core/src/ports/forge/contract.ts +258 -0
- package/packages/core/src/ports/forge/create.ts +74 -0
- package/packages/core/src/ports/forge/fake.ts +488 -0
- package/packages/core/src/ports/forge/github-pr-attachments.ts +351 -0
- package/packages/core/src/ports/forge/github-transport.ts +352 -0
- package/packages/core/src/ports/forge/github.ts +1162 -0
- package/packages/core/src/ports/forge/local-git.ts +568 -0
- package/packages/core/src/ports/runner/claude.ts +873 -0
- package/packages/core/src/ports/runner/cli-stream.ts +90 -0
- package/packages/core/src/ports/runner/codex.ts +873 -0
- package/packages/core/src/ports/runner/contract.ts +492 -0
- package/packages/core/src/ports/runner/fake.ts +213 -0
- package/packages/core/src/ports/runner/json-record.ts +2 -0
- package/packages/core/src/ports/runner/live-contract-fixture.ts +176 -0
- package/packages/core/src/ports/runner/one-shot.ts +29 -0
- package/packages/core/src/ports/runner/pi-bridge-core.ts +136 -0
- package/packages/core/src/ports/runner/pi-bridge.js +6 -0
- package/packages/core/src/ports/runner/pi-rpc.ts +454 -0
- package/packages/core/src/ports/runner/pi.ts +617 -0
- package/packages/core/src/ports/runner/production.ts +74 -0
- package/packages/core/src/ports/runner/provider-error.ts +121 -0
- package/packages/core/src/ports/runner/routing.ts +234 -0
- package/packages/core/src/ports/runner/runtime.ts +214 -0
- package/packages/core/src/ports/runner/session-env.ts +34 -0
- package/packages/core/src/ports/runner/stream-parts.ts +103 -0
- package/packages/core/src/ports/tickets/contract.ts +570 -0
- package/packages/core/src/ports/tickets/create.ts +135 -0
- package/packages/core/src/ports/tickets/fake.ts +215 -0
- package/packages/core/src/ports/tickets/file.ts +559 -0
- package/packages/core/src/ports/tickets/linear.ts +811 -0
- package/packages/core/src/ports/tickets/operations.ts +161 -0
- package/packages/core/src/ports/tickets/remote-index.ts +13 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +80 -0
- package/packages/core/src/ports/tickets/remote-server.ts +185 -0
- package/packages/core/src/ports/tickets/remote.ts +130 -0
- package/packages/core/src/ports/tickets/update.ts +51 -0
- package/packages/core/src/ports/types.ts +528 -0
- package/packages/core/src/ports/workspace/build-execution.ts +123 -0
- package/packages/core/src/ports/workspace/contract.ts +136 -0
- package/packages/core/src/ports/workspace/create.ts +132 -0
- package/packages/core/src/ports/workspace/distribution-archive.ts +317 -0
- package/packages/core/src/ports/workspace/fake.ts +432 -0
- package/packages/core/src/ports/workspace/git-worktree.ts +848 -0
- package/packages/core/src/ports/workspace/harvest-execution.ts +111 -0
- package/packages/core/src/ports/workspace/in-process-build-execution.ts +30 -0
- package/packages/core/src/ports/workspace/local-build-execution.ts +88 -0
- package/packages/core/src/ports/workspace/operator-sandbox.ts +142 -0
- package/packages/core/src/ports/workspace/phase-scratch.ts +156 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +2541 -0
- package/packages/core/src/processes/build-child-exit.ts +78 -0
- package/packages/core/src/processes/build-child.ts +119 -0
- package/packages/core/src/processes/build-execution-state.ts +95 -0
- package/packages/core/src/processes/build-parent-watch.ts +35 -0
- package/packages/core/src/processes/build-runner.ts +2614 -0
- package/packages/core/src/processes/dispatcher.ts +2825 -0
- package/packages/core/src/processes/execution-settlement.ts +171 -0
- package/packages/core/src/processes/harvest-child.ts +115 -0
- package/packages/core/src/processes/harvest-execution-state.ts +121 -0
- package/packages/core/src/processes/harvest-runner.ts +1656 -0
- package/packages/core/src/processes/harvest.ts +595 -0
- package/packages/core/src/processes/infrastructure-failure-budget.ts +146 -0
- package/packages/core/src/processes/phase-failure-budget.ts +52 -0
- package/packages/core/src/processes/process-group-reaper.ts +35 -0
- package/packages/core/src/processes/process-group.ts +65 -0
- package/packages/core/src/processes/publication-loss.ts +62 -0
- package/packages/core/src/processes/publication-state.ts +93 -0
- package/packages/core/src/processes/sandbox-state.ts +101 -0
- package/packages/core/src/registry.ts +24 -0
- package/packages/core/src/sessions/reducer.ts +185 -0
- package/packages/core/src/skills.ts +7 -0
- package/packages/core/src/spec-standard.ts +47 -0
- package/packages/core/src/store/adapter.ts +80 -0
- package/packages/core/src/store/build-scope.ts +272 -0
- package/packages/core/src/store/contract.ts +2581 -0
- package/packages/core/src/store/local/blobs.ts +38 -0
- package/packages/core/src/store/local/schema.ts +180 -0
- package/packages/core/src/store/local/store.ts +1713 -0
- package/packages/core/src/store/memory.ts +1138 -0
- package/packages/core/src/store/phase-session.ts +425 -0
- package/packages/core/src/store/remote/client.ts +780 -0
- package/packages/core/src/store/remote/index.ts +12 -0
- package/packages/core/src/store/remote/protocol.ts +348 -0
- package/packages/core/src/store/remote/server.ts +923 -0
- package/packages/core/src/store/remote/token.ts +137 -0
- package/packages/core/src/store/remote/version.ts +10 -0
- package/packages/core/src/store/retention.ts +91 -0
- package/packages/core/src/store/session-handle.ts +265 -0
- package/packages/core/src/store/streams/assemble.ts +79 -0
- package/packages/core/src/store/streams/lock.ts +42 -0
- package/packages/core/src/store/streams/session-writer.ts +306 -0
- package/packages/core/src/store/streams/types.ts +171 -0
- package/packages/core/src/store/streams/wait.ts +127 -0
- package/packages/core/src/store/subscribe.ts +105 -0
- package/packages/core/src/store/types.ts +466 -0
- package/packages/core/src/testing/fixed.ts +44 -0
- package/packages/core/src/testing/store-failures.ts +32 -0
- package/packages/core/src/zod-issues.ts +91 -0
- package/skills/code-review/SKILL.md +100 -0
- package/skills/finalize/SKILL.md +44 -0
- package/skills/guide/SKILL.md +1781 -0
- package/skills/guide/references/operator-api.md +164 -0
- package/skills/guide/references/plugin-authoring.md +354 -0
- package/skills/guide/references/remote-store-protocol.md +972 -0
- package/skills/guide/references/remote-ticket-protocol.md +79 -0
- package/skills/guide/references/setup.md +289 -0
- package/skills/guide/references/spec-standard.md +63 -0
- package/skills/guide/references/ticket-dependencies.md +59 -0
- package/skills/harvest/SKILL.md +66 -0
- package/skills/harvest-review/SKILL.md +63 -0
- package/skills/implement/SKILL.md +82 -0
- package/skills/plan/SKILL.md +123 -0
- package/skills/plan-review/SKILL.md +121 -0
- package/skills/reconcile/SKILL.md +65 -0
- package/skills/spec/SKILL.md +132 -0
- package/skills/tickets/SKILL.md +139 -0
- package/templates/autobuild.toml +36 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared polling implementation of `subscribe` (SPEC §7.2): push is the
|
|
3
|
+
* specced interface, polling `getEvents(since)` is the v2.0 implementation.
|
|
4
|
+
* Adapters delegate here so delivery semantics (in-order, exactly once per
|
|
5
|
+
* subscription, no overlapping polls) are identical everywhere.
|
|
6
|
+
*
|
|
7
|
+
* Two cadences (AUT-334). Without `waitSeconds` this is the unchanged
|
|
8
|
+
* interval loop: one immediate read, then one read per `pollMs`. With
|
|
9
|
+
* `waitSeconds` the loop long-polls: each cycle issues one held request and
|
|
10
|
+
* then gap-fills so request *starts* stay at least `pollMs` apart (elapsed
|
|
11
|
+
* request time counts toward the gap). Against a server that honors the
|
|
12
|
+
* bound, a quiet stream costs one request per wait window and a new event is
|
|
13
|
+
* delivered within about a server-side poll of its append; against a server
|
|
14
|
+
* that answers immediately regardless of `wait`, the cadence degrades to
|
|
15
|
+
* exactly today's request rate and delivery is unchanged.
|
|
16
|
+
*/
|
|
17
|
+
import type { AbEvent } from '../events/catalog'
|
|
18
|
+
import type { SubscribeOptions, Unsubscribe } from './types'
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_POLL_MS = 250
|
|
21
|
+
|
|
22
|
+
export type SubscribeRead = (
|
|
23
|
+
sinceSeq: number,
|
|
24
|
+
opts?: { waitSeconds?: number; signal?: AbortSignal },
|
|
25
|
+
) => Promise<AbEvent[]>
|
|
26
|
+
|
|
27
|
+
export function pollingSubscribe(
|
|
28
|
+
getEvents: SubscribeRead,
|
|
29
|
+
opts: SubscribeOptions,
|
|
30
|
+
onEvent: (event: AbEvent) => void,
|
|
31
|
+
): Unsubscribe {
|
|
32
|
+
let lastSeq = opts.fromSeq ?? 0
|
|
33
|
+
let stopped = false
|
|
34
|
+
let inFlight = false
|
|
35
|
+
|
|
36
|
+
const deliver = (events: AbEvent[]): void => {
|
|
37
|
+
for (const event of events) {
|
|
38
|
+
if (stopped) break
|
|
39
|
+
if (event.seq <= lastSeq) continue
|
|
40
|
+
lastSeq = event.seq
|
|
41
|
+
onEvent(event)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (opts.waitSeconds === undefined) {
|
|
46
|
+
const tick = async (): Promise<void> => {
|
|
47
|
+
if (stopped || inFlight) return
|
|
48
|
+
inFlight = true
|
|
49
|
+
try {
|
|
50
|
+
deliver(await getEvents(lastSeq))
|
|
51
|
+
} catch {
|
|
52
|
+
// Store unreachable — keep polling; the next tick retries (§8.7).
|
|
53
|
+
} finally {
|
|
54
|
+
inFlight = false
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const timer = setInterval(() => void tick(), opts.pollMs ?? DEFAULT_POLL_MS)
|
|
59
|
+
void tick()
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
stopped = true
|
|
63
|
+
clearInterval(timer)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Bounded-wait mode: one held request in flight at a time (the loop is
|
|
68
|
+
// sequential, so polls never overlap); request starts spaced at least
|
|
69
|
+
// `pollMs` apart. A throw from the read (store unreachable) ends the cycle
|
|
70
|
+
// and is retried after the gap, like the interval loop. Each cycle carries
|
|
71
|
+
// an abort signal so unsubscribe also cancels a held request — otherwise a
|
|
72
|
+
// long-poll subscription could leave a request dangling for a whole wait
|
|
73
|
+
// window after the caller stopped listening.
|
|
74
|
+
const pollMs = opts.pollMs ?? DEFAULT_POLL_MS
|
|
75
|
+
let inFlightRead: AbortController | undefined
|
|
76
|
+
void (async (): Promise<void> => {
|
|
77
|
+
while (!stopped) {
|
|
78
|
+
inFlight = true
|
|
79
|
+
const started = Date.now()
|
|
80
|
+
const controller = new AbortController()
|
|
81
|
+
inFlightRead = controller
|
|
82
|
+
try {
|
|
83
|
+
deliver(
|
|
84
|
+
await getEvents(lastSeq, {
|
|
85
|
+
waitSeconds: opts.waitSeconds,
|
|
86
|
+
signal: controller.signal,
|
|
87
|
+
}),
|
|
88
|
+
)
|
|
89
|
+
} catch {
|
|
90
|
+
// Store unreachable — retry after the gap (§8.7).
|
|
91
|
+
} finally {
|
|
92
|
+
inFlight = false
|
|
93
|
+
inFlightRead = undefined
|
|
94
|
+
}
|
|
95
|
+
if (stopped) break
|
|
96
|
+
const elapsed = Date.now() - started
|
|
97
|
+
if (elapsed < pollMs) await Bun.sleep(pollMs - elapsed)
|
|
98
|
+
}
|
|
99
|
+
})()
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
stopped = true
|
|
103
|
+
inFlightRead?.abort()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The BuildStore seam (SPEC §7): the durable home of everything a build
|
|
3
|
+
* produces — events, artifacts, transcripts — one logical place whether
|
|
4
|
+
* builds run locally or in ten remote sandboxes.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately narrow (§7.2): runners need `append`, `putArtifact`,
|
|
7
|
+
* `getArtifact`, `getEvents(since)`; operator UIs add `listBuilds` and
|
|
8
|
+
* `subscribe`. Liveness (lease + heartbeat) is mutable columns, never events
|
|
9
|
+
* (§15.2.6).
|
|
10
|
+
*
|
|
11
|
+
* Every adapter must satisfy the contract suite in `store/contract.ts`.
|
|
12
|
+
*/
|
|
13
|
+
import type { AbEvent, EventEnvelope, EventWrite } from '../events/catalog'
|
|
14
|
+
import type { EventType } from '../events/payloads'
|
|
15
|
+
import type {
|
|
16
|
+
RepositoryEvent,
|
|
17
|
+
RepositoryEventEnvelope,
|
|
18
|
+
RepositoryEventType,
|
|
19
|
+
RepositoryEventWrite,
|
|
20
|
+
} from '../events/repository'
|
|
21
|
+
import type {
|
|
22
|
+
SessionEvent,
|
|
23
|
+
SessionEventEnvelope,
|
|
24
|
+
SessionEventType,
|
|
25
|
+
SessionEventWrite,
|
|
26
|
+
} from '../events/sessions'
|
|
27
|
+
import type { TicketRef } from '../ontology'
|
|
28
|
+
import type {
|
|
29
|
+
StreamChunk,
|
|
30
|
+
StreamOutcome,
|
|
31
|
+
StreamPart,
|
|
32
|
+
StreamRead,
|
|
33
|
+
StreamRecord,
|
|
34
|
+
StreamScope,
|
|
35
|
+
} from './streams/types'
|
|
36
|
+
export type { StreamRecord, StreamScope }
|
|
37
|
+
|
|
38
|
+
/** Injectable time source — adapters take one so tests are deterministic. */
|
|
39
|
+
export type Clock = () => Date
|
|
40
|
+
|
|
41
|
+
export const systemClock: Clock = () => new Date()
|
|
42
|
+
|
|
43
|
+
export interface BuildRecord {
|
|
44
|
+
slug: string
|
|
45
|
+
repo: string
|
|
46
|
+
/** Normalized git origin URL of the build's repository, absent when the
|
|
47
|
+
* repo had no origin remote at dispatch time. A location-independent
|
|
48
|
+
* secondary identity: ambient reads may accept a differently located
|
|
49
|
+
* checkout of the same repository by origin equality (repo stays primary). */
|
|
50
|
+
repoOrigin?: string
|
|
51
|
+
ticket?: TicketRef
|
|
52
|
+
branch?: string
|
|
53
|
+
createdAt: string
|
|
54
|
+
updatedAt: string
|
|
55
|
+
/** Runner lease — mutable liveness, never events (§15.2.6). */
|
|
56
|
+
lease?: { holder: string; expiresAt: string }
|
|
57
|
+
heartbeatAt?: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface NewBuildInput {
|
|
61
|
+
slug: string
|
|
62
|
+
repo: string
|
|
63
|
+
/** See BuildRecord.repoOrigin. */
|
|
64
|
+
repoOrigin?: string
|
|
65
|
+
ticket?: TicketRef
|
|
66
|
+
branch?: string
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ArtifactMeta {
|
|
70
|
+
build: string
|
|
71
|
+
kind: string
|
|
72
|
+
/** 0-based per kind: first deposit of a kind is rev 0 (SPEC §6.3). */
|
|
73
|
+
revision: number
|
|
74
|
+
/** Content address: sha256 hex of the blob (SPEC §7.1). */
|
|
75
|
+
blobRef: string
|
|
76
|
+
metadata: Record<string, unknown>
|
|
77
|
+
createdAt: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface RepositoryRecord {
|
|
81
|
+
repo: string
|
|
82
|
+
createdAt: string
|
|
83
|
+
updatedAt: string
|
|
84
|
+
/** Repository workflow lease (harvest single-flight), separate from builds. */
|
|
85
|
+
lease?: { holder: string; expiresAt: string }
|
|
86
|
+
heartbeatAt?: string
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface RepositoryArtifactMeta {
|
|
90
|
+
repo: string
|
|
91
|
+
kind: string
|
|
92
|
+
revision: number
|
|
93
|
+
blobRef: string
|
|
94
|
+
metadata: Record<string, unknown>
|
|
95
|
+
createdAt: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface RepositoryArtifact {
|
|
99
|
+
meta: RepositoryArtifactMeta
|
|
100
|
+
content: Uint8Array
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** An operator session: hosted-only durable orchestrator-conversation state
|
|
104
|
+
* (SPEC §7.1.1). The record carries only identity and timestamps; everything
|
|
105
|
+
* else — status, open turn, pending approval, wake settings — is a reduction
|
|
106
|
+
* of the session's own event log (sessions/reducer.ts), never a column. */
|
|
107
|
+
export interface SessionRecord {
|
|
108
|
+
/** Store-assigned (`os_<uuid>`). */
|
|
109
|
+
id: string
|
|
110
|
+
repo: string
|
|
111
|
+
/** The owning operator's normalized identity (trimmed nonblank — the same
|
|
112
|
+
* normalization `OperatorTokenScope` applies). A session belongs to its
|
|
113
|
+
* creating operator. */
|
|
114
|
+
operator: string
|
|
115
|
+
title?: string
|
|
116
|
+
createdAt: string
|
|
117
|
+
updatedAt: string
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface NewSessionInput {
|
|
121
|
+
repo: string
|
|
122
|
+
operator: string
|
|
123
|
+
title?: string
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The operator identity's one normalization — trimmed, nonblank. The same
|
|
127
|
+
* normalization `OperatorTokenScope` applies to its `user` claim, so a token's
|
|
128
|
+
* user matches the session records it creates. */
|
|
129
|
+
export function normalizeOperator(operator: string): string {
|
|
130
|
+
const trimmed = operator.trim()
|
|
131
|
+
if (!trimmed) throw new Error('operator is required')
|
|
132
|
+
return trimmed
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface SessionArtifactMeta {
|
|
136
|
+
session: string
|
|
137
|
+
kind: string
|
|
138
|
+
/** 0-based per kind, like build and repository artifacts (§6.3). */
|
|
139
|
+
revision: number
|
|
140
|
+
blobRef: string
|
|
141
|
+
metadata: Record<string, unknown>
|
|
142
|
+
createdAt: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface SessionArtifact {
|
|
146
|
+
meta: SessionArtifactMeta
|
|
147
|
+
content: Uint8Array
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ArtifactInput {
|
|
151
|
+
kind: string
|
|
152
|
+
content: string | Uint8Array
|
|
153
|
+
metadata?: Record<string, unknown>
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface Artifact {
|
|
157
|
+
meta: ArtifactMeta
|
|
158
|
+
content: Uint8Array
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Content-addressed blob storage (SPEC §7.1) — a plain directory locally,
|
|
163
|
+
* any object store remotely. The database stores refs, never bulk content.
|
|
164
|
+
* The interface is deliberately this narrow; do not widen it.
|
|
165
|
+
*/
|
|
166
|
+
export interface BlobStore {
|
|
167
|
+
put(hash: string, bytes: Uint8Array): Promise<void>
|
|
168
|
+
get(hash: string): Promise<Uint8Array | null>
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export type Unsubscribe = () => void
|
|
172
|
+
|
|
173
|
+
export interface SubscribeOptions {
|
|
174
|
+
/** Deliver events with seq strictly greater than this (default 0 = all). */
|
|
175
|
+
fromSeq?: number
|
|
176
|
+
/** Poll interval for the v2.0 polling implementation (§7.2). In bounded-wait
|
|
177
|
+
* mode (`waitSeconds` set) it is the floor between request *starts* —
|
|
178
|
+
* elapsed request time counts toward the gap. */
|
|
179
|
+
pollMs?: number
|
|
180
|
+
/** Bounded-wait window in whole seconds. When set against a store whose
|
|
181
|
+
* event reads honor the bound (an `http(s)` store's server holds the
|
|
182
|
+
* request), each poll cycle issues one held request instead of polling
|
|
183
|
+
* every `pollMs`; delivery latency drops to about one server-side poll.
|
|
184
|
+
* The adapter clamps at its own ceiling; it never rejects. */
|
|
185
|
+
waitSeconds?: number
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export interface BuildStore {
|
|
189
|
+
/** Return an interface-enforced handle with authority over exactly `slug`.
|
|
190
|
+
* The handle retains this shape so foreign/admin calls fail loudly. */
|
|
191
|
+
scopeBuild(slug: string): BuildScopedStore
|
|
192
|
+
|
|
193
|
+
createBuild(input: NewBuildInput): Promise<BuildRecord>
|
|
194
|
+
getBuild(slug: string): Promise<BuildRecord | null>
|
|
195
|
+
listBuilds(): Promise<BuildRecord[]>
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Append one validated event; the store assigns `seq` (per-build,
|
|
199
|
+
* monotonic, starting at 1) and `ts` (§15.1). Writes must pass
|
|
200
|
+
* `validateEventWrite` — invalid events throw `EventValidationError`.
|
|
201
|
+
*/
|
|
202
|
+
append<T extends EventType>(slug: string, event: EventWrite<T>): Promise<EventEnvelope<T>>
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Atomically append one validated event only when the build stream's
|
|
206
|
+
* current sequence equals `expectedSeq` (0 for an empty stream). Returns
|
|
207
|
+
* null when the stream has advanced. A comparison miss must not mutate the
|
|
208
|
+
* stream or build timestamps. Invalid events and unknown builds reject just
|
|
209
|
+
* as they do for `append`.
|
|
210
|
+
*/
|
|
211
|
+
appendIfCurrent<T extends EventType>(
|
|
212
|
+
slug: string,
|
|
213
|
+
expectedSeq: number,
|
|
214
|
+
event: EventWrite<T>,
|
|
215
|
+
): Promise<EventEnvelope<T> | null>
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Atomic deposit (D6): store artifacts, then append the event that
|
|
219
|
+
* references them — one operation, no state where an artifact exists
|
|
220
|
+
* without its event or vice versa. `makeEvent` receives the deposited
|
|
221
|
+
* metas (with assigned revisions) so the payload can carry `{kind, rev}`
|
|
222
|
+
* refs. If the event fails validation, the artifact deposit is rolled
|
|
223
|
+
* back (orphaned blobs are harmless — they are content-addressed).
|
|
224
|
+
*
|
|
225
|
+
* Ordering invariant (AUT-322): the adapter must run `validateEventWrite`
|
|
226
|
+
* on the batch event *before* any retention prune (store/retention.ts) of
|
|
227
|
+
* artifacts in the same deposit batch, so a same-kind batch larger than
|
|
228
|
+
* the retention bound never loses a sibling to a prune that outran
|
|
229
|
+
* validation; on validation failure the batch — deposits and prunes —
|
|
230
|
+
* must leave no trace. `revisionsToPrune` is a pure function of the full
|
|
231
|
+
* post-batch revision set, so pruning once per distinct batch kind after
|
|
232
|
+
* validation is equivalent to per-deposit pruning.
|
|
233
|
+
*/
|
|
234
|
+
appendWithArtifacts<T extends EventType>(
|
|
235
|
+
slug: string,
|
|
236
|
+
artifacts: ArtifactInput[],
|
|
237
|
+
makeEvent: (deposited: ArtifactMeta[]) => EventWrite<T>,
|
|
238
|
+
): Promise<{ event: EventEnvelope<T>; artifacts: ArtifactMeta[] }>
|
|
239
|
+
|
|
240
|
+
/** Events with seq strictly greater than `sinceSeq` (default 0), in order.
|
|
241
|
+
* When nothing newer exists and `opts.waitSeconds` is given (whole
|
|
242
|
+
* seconds; clamped at the adapter's ceiling, never rejected), the adapter
|
|
243
|
+
* honors the bound: it returns no later than the bound and as soon as an
|
|
244
|
+
* event with `seq > sinceSeq` is appended; when such events already exist
|
|
245
|
+
* it returns immediately. Omitting `waitSeconds` (or 0) is the immediate
|
|
246
|
+
* form.
|
|
247
|
+
*
|
|
248
|
+
* `opts.signal` lets a caller cancel the held read before its bound (a
|
|
249
|
+
* watcher or a disconnected peer torn down mid-hold). When the signal
|
|
250
|
+
* fires, the adapter resolves with its current result — the empty read it
|
|
251
|
+
* would otherwise keep polling for — and stops polling; it MUST NOT wait
|
|
252
|
+
* past the bound because of the signal, and a read that finds data before
|
|
253
|
+
* the abort still returns it. */
|
|
254
|
+
getEvents(
|
|
255
|
+
slug: string,
|
|
256
|
+
sinceSeq?: number,
|
|
257
|
+
opts?: { waitSeconds?: number; signal?: AbortSignal },
|
|
258
|
+
): Promise<AbEvent[]>
|
|
259
|
+
|
|
260
|
+
putArtifact(slug: string, artifact: ArtifactInput): Promise<ArtifactMeta>
|
|
261
|
+
/** Latest revision when `rev` is omitted; null if kind (or rev) absent. */
|
|
262
|
+
getArtifact(slug: string, kind: string, rev?: number): Promise<Artifact | null>
|
|
263
|
+
listArtifacts(slug: string, kind?: string): Promise<ArtifactMeta[]>
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Take or renew the runner lease. Succeeds when unheld, expired, or
|
|
267
|
+
* already held by `holder` (renewal). Claiming is how a new sandbox takes
|
|
268
|
+
* over a dead one's build (§7.4, §15.6-C).
|
|
269
|
+
*/
|
|
270
|
+
claimLease(slug: string, holder: string, ttlMs: number): Promise<boolean>
|
|
271
|
+
/** Bump liveness; false (no-op) unless `holder` holds an unexpired lease. */
|
|
272
|
+
heartbeat(slug: string, holder: string): Promise<boolean>
|
|
273
|
+
releaseLease(slug: string, holder: string): Promise<void>
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Push is the specced interface; the v2.0 implementation is polling
|
|
277
|
+
* `getEvents(since)` (§7.2). Events are delivered in seq order, each
|
|
278
|
+
* exactly once per subscription.
|
|
279
|
+
*/
|
|
280
|
+
subscribe(slug: string, opts: SubscribeOptions, onEvent: (event: AbEvent) => void): Unsubscribe
|
|
281
|
+
|
|
282
|
+
// ── Repository journal (outer-loop workflows and controls) ───────────────
|
|
283
|
+
// Kept alongside, not inside, build streams: repository state is not a build.
|
|
284
|
+
ensureRepo(repo: string): Promise<RepositoryRecord>
|
|
285
|
+
getRepo(repo: string): Promise<RepositoryRecord | null>
|
|
286
|
+
appendRepo<T extends RepositoryEventType>(
|
|
287
|
+
repo: string,
|
|
288
|
+
event: RepositoryEventWrite<T>,
|
|
289
|
+
): Promise<RepositoryEventEnvelope<T>>
|
|
290
|
+
appendRepoWithArtifacts<T extends RepositoryEventType>(
|
|
291
|
+
repo: string,
|
|
292
|
+
artifacts: ArtifactInput[],
|
|
293
|
+
makeEvent: (deposited: RepositoryArtifactMeta[]) => RepositoryEventWrite<T>,
|
|
294
|
+
): Promise<{
|
|
295
|
+
event: RepositoryEventEnvelope<T>
|
|
296
|
+
artifacts: RepositoryArtifactMeta[]
|
|
297
|
+
}>
|
|
298
|
+
/** Same atomic-deposit and ordering contracts as `appendWithArtifacts`:
|
|
299
|
+
* the batch event is validated before any retention prune of artifacts in
|
|
300
|
+
* the same deposit batch (AUT-322), and an invalid event leaves no trace.
|
|
301
|
+
* The optional bounded wait mirrors `getEvents` exactly, including the
|
|
302
|
+
* resolve-with-current-result abort contract of `opts.signal`. */
|
|
303
|
+
getRepoEvents(
|
|
304
|
+
repo: string,
|
|
305
|
+
sinceSeq?: number,
|
|
306
|
+
opts?: { waitSeconds?: number; signal?: AbortSignal },
|
|
307
|
+
): Promise<RepositoryEvent[]>
|
|
308
|
+
putRepoArtifact(repo: string, artifact: ArtifactInput): Promise<RepositoryArtifactMeta>
|
|
309
|
+
getRepoArtifact(repo: string, kind: string, rev?: number): Promise<RepositoryArtifact | null>
|
|
310
|
+
listRepoArtifacts(repo: string, kind?: string): Promise<RepositoryArtifactMeta[]>
|
|
311
|
+
claimRepoLease(repo: string, holder: string, ttlMs: number): Promise<boolean>
|
|
312
|
+
heartbeatRepo(repo: string, holder: string): Promise<boolean>
|
|
313
|
+
releaseRepoLease(repo: string, holder: string): Promise<void>
|
|
314
|
+
|
|
315
|
+
// ── Operator sessions (SPEC §7.1.1 — a third resource kind) ────────────
|
|
316
|
+
// Hosted-only durable orchestrator-conversation state: a record keyed by
|
|
317
|
+
// repository and operator, its own small typed event catalog
|
|
318
|
+
// (events/sessions.ts) with a per-session sequence, and session-scoped
|
|
319
|
+
// streams. The record and its first fact (`session.created`) land together;
|
|
320
|
+
// every later fact appends through the same validated gate as builds and
|
|
321
|
+
// the repository journal. Nothing local creates sessions; adapters merely
|
|
322
|
+
// implement the contract uniformly.
|
|
323
|
+
/** Create the record and atomically append `session.created` (seq 1,
|
|
324
|
+
* actor the operator). Unlike builds, nothing else ever creates sessions. */
|
|
325
|
+
createSession(input: NewSessionInput): Promise<SessionRecord>
|
|
326
|
+
getSession(id: string): Promise<SessionRecord | null>
|
|
327
|
+
/** Every session of `repo` in creation order: `createdAt` ascending, with
|
|
328
|
+
* same-millisecond ties broken by a store-assigned monotonic creation
|
|
329
|
+
* sequence — never by the random `os_<uuid>` id. The counter is assigned at
|
|
330
|
+
* `createSession`, is never reused (sessions are never deleted), and is not
|
|
331
|
+
* part of `SessionRecord`. This makes the returned order a function of the
|
|
332
|
+
* creation history alone, so every adapter (memory, SQLite, Postgres,
|
|
333
|
+
* remote) returns the same order for the same creation history. */
|
|
334
|
+
listSessions(repo: string): Promise<SessionRecord[]>
|
|
335
|
+
/** Append one validated session event; the store assigns the per-session
|
|
336
|
+
* `seq` (monotonic, from 1) and `ts`. Unknown sessions reject; invalid
|
|
337
|
+
* events throw `EventValidationError` and append nothing. */
|
|
338
|
+
appendSessionEvent<T extends SessionEventType>(
|
|
339
|
+
id: string,
|
|
340
|
+
event: SessionEventWrite<T>,
|
|
341
|
+
): Promise<SessionEventEnvelope<T>>
|
|
342
|
+
/** Events with seq strictly greater than `sinceSeq` (default 0), in order.
|
|
343
|
+
* When nothing newer exists, an adapter honors `waitSeconds` with the
|
|
344
|
+
* same clamp/early-return rules as stream reads (§7.6): whole seconds,
|
|
345
|
+
* above 30 clamped to 30, returning as soon as an event lands.
|
|
346
|
+
* `opts.signal` carries the same resolve-with-current-result abort
|
|
347
|
+
* contract as `getEvents`: on abort the adapter stops polling and resolves
|
|
348
|
+
* with its current result, never waiting past the bound. */
|
|
349
|
+
getSessionEvents(
|
|
350
|
+
id: string,
|
|
351
|
+
sinceSeq?: number,
|
|
352
|
+
opts?: { waitSeconds?: number; signal?: AbortSignal },
|
|
353
|
+
): Promise<SessionEvent[]>
|
|
354
|
+
/** Same atomic-deposit and validation-before-prune contracts as
|
|
355
|
+
* `appendWithArtifacts` (D6, AUT-322): the batch event is validated before
|
|
356
|
+
* any retention prune of artifacts in the same deposit batch, and an
|
|
357
|
+
* invalid event leaves no trace. */
|
|
358
|
+
appendSessionWithArtifacts<T extends SessionEventType>(
|
|
359
|
+
id: string,
|
|
360
|
+
artifacts: ArtifactInput[],
|
|
361
|
+
makeEvent: (deposited: SessionArtifactMeta[]) => SessionEventWrite<T>,
|
|
362
|
+
): Promise<{ event: SessionEventEnvelope<T>; artifacts: SessionArtifactMeta[] }>
|
|
363
|
+
putSessionArtifact(id: string, artifact: ArtifactInput): Promise<SessionArtifactMeta>
|
|
364
|
+
/** Latest revision when `rev` is omitted; null if kind (or rev) absent. */
|
|
365
|
+
getSessionArtifact(id: string, kind: string, rev?: number): Promise<SessionArtifact | null>
|
|
366
|
+
listSessionArtifacts(id: string, kind?: string): Promise<SessionArtifactMeta[]>
|
|
367
|
+
/** Return an interface-enforced handle with authority over exactly one
|
|
368
|
+
* session: its record, events, artifacts, and session-scoped streams. */
|
|
369
|
+
scopeSession(id: string): SessionScopedStore
|
|
370
|
+
|
|
371
|
+
// ── Streams (SPEC §7.6 — the third primitive) ───────────────────────────
|
|
372
|
+
// An append-only, per-stream sequenced log of protocol parts with an
|
|
373
|
+
// open-then-closed lifecycle that finalizes into an artifact. Presentation,
|
|
374
|
+
// never routing: no kernel, engine, reducer, or dispatcher decision reads
|
|
375
|
+
// stream content; outcomes travel only the typed CLI. Chunk vocabulary:
|
|
376
|
+
// the AI SDK UI Message Stream protocol (`ai-ui-message-stream/v1`).
|
|
377
|
+
//
|
|
378
|
+
// Stream ids are store-assigned (`st_<uuid>`); the scope is fixed at
|
|
379
|
+
// create and every operation addresses the stream by id.
|
|
380
|
+
|
|
381
|
+
/** Create an open stream under `scope`. Store-assigned id and
|
|
382
|
+
* `createdAt`; `format` is the literal `ai-ui-message-stream/v1`. */
|
|
383
|
+
createStream(scope: StreamScope, label: string): Promise<StreamRecord>
|
|
384
|
+
|
|
385
|
+
/** Append one batch of parts. Each part must be a JSON object whose `type`
|
|
386
|
+
* is a nonempty string; the store performs no further protocol validation
|
|
387
|
+
* on append. The store assigns the per-stream sequence (from 1) and the
|
|
388
|
+
* timestamp. A batch whose serialized size exceeds 1,048,576 bytes rejects
|
|
389
|
+
* with `StreamBatchTooLargeError` without mutation; a closed or unknown
|
|
390
|
+
* stream rejects (`StreamClosedError` / `Error`) and writes nothing. */
|
|
391
|
+
appendStreamParts(streamId: string, parts: StreamPart[]): Promise<StreamChunk>
|
|
392
|
+
|
|
393
|
+
/** Chunks with sequence strictly greater than `since` (default 0), in
|
|
394
|
+
* order, plus the stream's current status and, when closed, its outcome
|
|
395
|
+
* and artifact reference. When no newer chunk exists and the stream is
|
|
396
|
+
* open, an adapter honors `waitSeconds` (whole seconds; above 30 clamped
|
|
397
|
+
* to 30): it returns no later than the bound and as soon as a chunk is
|
|
398
|
+
* appended or the stream closes. Closed streams never wait.
|
|
399
|
+
* `opts.signal` carries the same resolve-with-current-result abort
|
|
400
|
+
* contract as `getEvents`: on abort the adapter stops polling and resolves
|
|
401
|
+
* with its current result, never waiting past the bound. */
|
|
402
|
+
readStream(
|
|
403
|
+
streamId: string,
|
|
404
|
+
opts?: { since?: number; waitSeconds?: number; signal?: AbortSignal },
|
|
405
|
+
): Promise<StreamRead>
|
|
406
|
+
|
|
407
|
+
/** Close with an outcome. One atomic operation: assemble the chunks into
|
|
408
|
+
* the protocol's `UIMessage[]` document, deposit it as an artifact
|
|
409
|
+
* (`stream:<streamId>`, revision 0) on the owning scope, and mark the
|
|
410
|
+
* stream closed. If the artifact deposit fails the stream stays open and
|
|
411
|
+
* nothing is written. Closing an already-closed stream is a no-op
|
|
412
|
+
* returning the record. */
|
|
413
|
+
closeStream(streamId: string, outcome: StreamOutcome): Promise<StreamRecord>
|
|
414
|
+
|
|
415
|
+
/** The stream record, or null when the id is unknown. */
|
|
416
|
+
getStream(streamId: string): Promise<StreamRecord | null>
|
|
417
|
+
|
|
418
|
+
/** Every stream in `scope` in creation order: `createdAt` ascending, with
|
|
419
|
+
* same-millisecond ties broken by a store-assigned monotonic creation
|
|
420
|
+
* sequence — never by the random `st_<uuid>` id. The counter is assigned at
|
|
421
|
+
* `createStream`, is never reused (streams are never deleted — retention
|
|
422
|
+
* prunes `stream_chunks` only), and is not part of `StreamRecord`. This
|
|
423
|
+
* makes the returned order a function of the creation history alone, so
|
|
424
|
+
* every adapter (memory, SQLite, Postgres, remote) returns the same order
|
|
425
|
+
* for the same creation history. */
|
|
426
|
+
listStreams(scope: StreamScope): Promise<StreamRecord[]>
|
|
427
|
+
|
|
428
|
+
close(): Promise<void>
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** A BuildStore constrained to one build stream. `buildScope` is immutable;
|
|
432
|
+
* attempting to re-scope to another slug is an authority error. */
|
|
433
|
+
export interface BuildScopedStore extends BuildStore {
|
|
434
|
+
readonly buildScope: string
|
|
435
|
+
scopeBuild(slug: string): BuildScopedStore
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** A BuildStore constrained to one operator session. `sessionScope` is
|
|
439
|
+
* immutable; the handle may touch only its own session's events, artifacts,
|
|
440
|
+
* and session-scoped streams, and rejects session create/list and every
|
|
441
|
+
* build/repository operation. This is the (later turn runner's) authority
|
|
442
|
+
* over exactly one session, mirroring `BuildScopedStore`. */
|
|
443
|
+
export interface SessionScopedStore extends BuildStore {
|
|
444
|
+
readonly sessionScope: string
|
|
445
|
+
scopeSession(id: string): SessionScopedStore
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/** Shared runtime guard for in-process conditional append callers. */
|
|
449
|
+
export function validateExpectedSeq(expectedSeq: number): void {
|
|
450
|
+
if (!Number.isInteger(expectedSeq) || expectedSeq < 0) {
|
|
451
|
+
throw new Error(`expectedSeq must be a nonnegative integer, got ${expectedSeq}`)
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** sha256 hex — the content address for blobs (SPEC §7.1). */
|
|
456
|
+
export function contentHash(bytes: Uint8Array): string {
|
|
457
|
+
return new Bun.CryptoHasher('sha256').update(bytes).digest('hex')
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function toBytes(content: string | Uint8Array): Uint8Array {
|
|
461
|
+
return typeof content === 'string' ? new TextEncoder().encode(content) : content
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
export function textContent(artifact: Artifact): string {
|
|
465
|
+
return new TextDecoder().decode(artifact.content)
|
|
466
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic test doubles shared across the suite. Production code never
|
|
3
|
+
* imports from `src/testing/`.
|
|
4
|
+
*/
|
|
5
|
+
import type { Clock } from '../store/types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A clock that starts at `start` and advances `stepMs` per call — so every
|
|
9
|
+
* store-assigned `ts` in a test is distinct and predictable.
|
|
10
|
+
*/
|
|
11
|
+
export function steppingClock(
|
|
12
|
+
start = '2026-07-15T12:00:00.000Z',
|
|
13
|
+
stepMs = 1000,
|
|
14
|
+
): Clock & { advance(ms: number): void } {
|
|
15
|
+
let now = new Date(start).getTime()
|
|
16
|
+
const clock = (() => {
|
|
17
|
+
const current = new Date(now)
|
|
18
|
+
now += stepMs
|
|
19
|
+
return current
|
|
20
|
+
}) as Clock & { advance(ms: number): void }
|
|
21
|
+
clock.advance = (ms: number) => {
|
|
22
|
+
now += ms
|
|
23
|
+
}
|
|
24
|
+
return clock
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A clock frozen at `at` until manually advanced. */
|
|
28
|
+
export function manualClock(at = '2026-07-15T12:00:00.000Z'): Clock & {
|
|
29
|
+
advance(ms: number): void
|
|
30
|
+
set(iso: string): void
|
|
31
|
+
} {
|
|
32
|
+
let now = new Date(at).getTime()
|
|
33
|
+
const clock = (() => new Date(now)) as Clock & {
|
|
34
|
+
advance(ms: number): void
|
|
35
|
+
set(iso: string): void
|
|
36
|
+
}
|
|
37
|
+
clock.advance = (ms: number) => {
|
|
38
|
+
now += ms
|
|
39
|
+
}
|
|
40
|
+
clock.set = (iso: string) => {
|
|
41
|
+
now = new Date(iso).getTime()
|
|
42
|
+
}
|
|
43
|
+
return clock
|
|
44
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Store decorators that inject deterministic failures. Nothing here involves
|
|
3
|
+
* timing: a decorator fails one named operation and delegates the rest, so a
|
|
4
|
+
* test can pin exactly what a surviving PREFIX of a multi-write action leaves
|
|
5
|
+
* behind — which is the whole reason write ORDER is load-bearing in actions
|
|
6
|
+
* the store cannot make atomic.
|
|
7
|
+
*
|
|
8
|
+
* Production code never imports from `src/testing/`.
|
|
9
|
+
*/
|
|
10
|
+
import type {
|
|
11
|
+
RepositoryEventEnvelope,
|
|
12
|
+
RepositoryEventType,
|
|
13
|
+
RepositoryEventWrite,
|
|
14
|
+
} from '../events/repository'
|
|
15
|
+
import type { BuildStore } from '../store/types'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Fail every `appendRepo` of one repository event type; delegate everything
|
|
19
|
+
* else to `store`. The `Object.create` idiom keeps the decorator a real
|
|
20
|
+
* `BuildStore` without restating the port.
|
|
21
|
+
*/
|
|
22
|
+
export function withFailingRepoAppend(store: BuildStore, failOn: RepositoryEventType): BuildStore {
|
|
23
|
+
const proxy = Object.create(store) as BuildStore
|
|
24
|
+
proxy.appendRepo = async <T extends RepositoryEventType>(
|
|
25
|
+
repo: string,
|
|
26
|
+
event: RepositoryEventWrite<T>,
|
|
27
|
+
): Promise<RepositoryEventEnvelope<T>> => {
|
|
28
|
+
if (event.type === failOn) throw new Error(`store failure writing ${failOn}`)
|
|
29
|
+
return store.appendRepo(repo, event)
|
|
30
|
+
}
|
|
31
|
+
return proxy
|
|
32
|
+
}
|