@defrex/autobuild 0.8.0 → 0.9.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 +313 -26
- package/bin/ab-build-runner.ts +8 -2
- package/docs/architecture.md +57 -14
- package/docs/assets/headline-wide.png +0 -0
- package/docs/configuration.md +151 -25
- package/docs/hosted-dispatcher.md +69 -13
- package/docs/operator-api.md +2 -2
- package/docs/remote-store-protocol.md +169 -16
- package/docs/remote-ticket-protocol.md +1 -1
- package/docs/setup.md +12 -9
- package/docs/website.md +10 -0
- package/package.json +43 -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/model.ts +57 -7
- package/packages/core/src/cli/dashboard/poll.ts +69 -10
- 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.ts +320 -46
- package/packages/core/src/cli/harvest.ts +122 -8
- package/packages/core/src/cli/help.ts +47 -8
- 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 +248 -222
- 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/status.ts +149 -27
- 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 +307 -7
- package/packages/core/src/cli/wait.ts +147 -39
- package/packages/core/src/cli/watch.ts +128 -166
- package/packages/core/src/config/live.ts +22 -0
- package/packages/core/src/config/pipeline-source.ts +376 -0
- package/packages/core/src/config/schema.ts +230 -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 +2 -1
- package/packages/core/src/kernel/auto-merge-default.ts +123 -0
- package/packages/core/src/kernel/phases.ts +16 -2
- package/packages/core/src/kernel/reducer.ts +40 -1
- 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 +208 -19
- 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/{remote-index.ts → hosted-index.ts} +3 -2
- package/packages/core/src/ports/types.ts +17 -2
- 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 +37 -0
- package/packages/core/src/processes/build-runner.ts +54 -3
- package/packages/core/src/processes/dispatcher.ts +247 -41
- package/packages/core/src/processes/harvest-child.ts +5 -0
- package/packages/core/src/processes/harvest-runner.ts +66 -19
- package/packages/core/src/processes/harvest.ts +196 -24
- package/packages/core/src/processes/orchestrator-tick.ts +319 -0
- package/packages/core/src/processes/sandbox-state.ts +10 -0
- package/packages/core/src/store/adapter.ts +32 -0
- package/packages/core/src/store/build-scope.ts +64 -0
- package/packages/core/src/store/contract.ts +868 -0
- package/packages/core/src/store/digest.ts +49 -0
- package/packages/core/src/store/local/schema.ts +29 -2
- package/packages/core/src/store/local/store.ts +338 -4
- package/packages/core/src/store/memory.ts +179 -0
- package/packages/core/src/store/new-build.ts +17 -0
- package/packages/core/src/store/phase-session.ts +71 -0
- package/packages/core/src/store/remote/client.ts +260 -1
- package/packages/core/src/store/remote/index.ts +12 -2
- package/packages/core/src/store/remote/protocol.ts +100 -12
- package/packages/core/src/store/remote/version.ts +6 -2
- package/packages/core/src/store/repo-state-events.ts +140 -0
- package/packages/core/src/store/retention.ts +7 -0
- package/packages/core/src/store/session-handle.ts +64 -0
- package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +28 -9
- 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 +425 -0
- package/packages/core/src/store/types.ts +122 -1
- package/packages/core/src/testing/index.ts +59 -0
- package/skills/code-review/SKILL.md +5 -1
- package/skills/guide/SKILL.md +126 -33
- 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 +169 -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/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,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared build-digest derivation (AUT-487): every BuildStore adapter
|
|
3
|
+
* answers `getRepoBuildDigests` by fetching only the digest-relevant event
|
|
4
|
+
* types from its existing event table and running this one pure function per
|
|
5
|
+
* build — identical results by construction, and the digest's `terminal` can
|
|
6
|
+
* never drift from `reduceBuild`'s terminal rule because this is the same
|
|
7
|
+
* in-order-overwrite of the latest terminal fact (§15.5). AUT-521 widens the
|
|
8
|
+
* projection with the observation timestamps and merge facts the harvest
|
|
9
|
+
* pressure gate needs, keeping that gate's cost flat in accumulated build
|
|
10
|
+
* history.
|
|
11
|
+
*/
|
|
12
|
+
import type { AbEvent } from '../events/catalog'
|
|
13
|
+
import type { BuildDigest } from './types'
|
|
14
|
+
|
|
15
|
+
/** The event types a digest is derived from. Adapters filter their event
|
|
16
|
+
* rows to exactly these, so a per-build type-filtered index keeps the scan's
|
|
17
|
+
* cost growing with the actual signal, not with total history. */
|
|
18
|
+
export const DIGEST_EVENT_TYPES = [
|
|
19
|
+
'build.completed',
|
|
20
|
+
'build.aborted',
|
|
21
|
+
'observation.recorded',
|
|
22
|
+
'pr.merged',
|
|
23
|
+
] as const
|
|
24
|
+
|
|
25
|
+
/** The digest-relevant projection of one build's events, in log order.
|
|
26
|
+
* `terminal` follows `reduceBuild` exactly: `build.completed` sets `done`,
|
|
27
|
+
* `build.aborted` sets `aborted`, latest wins (in-order overwrite) — and
|
|
28
|
+
* `merged` follows the same overwrite pattern for `pr.merged` timestamps.
|
|
29
|
+
* The events carry no slug, so the result is slug-less; every adapter attaches
|
|
30
|
+
* `slug` itself when grouping its per-build event rows. */
|
|
31
|
+
export function reduceBuildDigest(
|
|
32
|
+
events: Pick<AbEvent, 'type' | 'seq' | 'ts'>[],
|
|
33
|
+
): Omit<BuildDigest, 'slug'> {
|
|
34
|
+
let terminal: BuildDigest['terminal']
|
|
35
|
+
let merged: string | undefined
|
|
36
|
+
const observations: { seq: number; ts: string }[] = []
|
|
37
|
+
for (const event of events) {
|
|
38
|
+
if (event.type === 'build.completed') terminal = 'done'
|
|
39
|
+
else if (event.type === 'build.aborted') terminal = 'aborted'
|
|
40
|
+
else if (event.type === 'pr.merged') merged = event.ts
|
|
41
|
+
else if (event.type === 'observation.recorded')
|
|
42
|
+
observations.push({ seq: event.seq, ts: event.ts })
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
observations,
|
|
46
|
+
...(merged !== undefined ? { merged } : {}),
|
|
47
|
+
...(terminal !== undefined ? { terminal } : {}),
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* be kept in lockstep with this schema (this file is the drizzle-kit source
|
|
10
10
|
* of truth).
|
|
11
11
|
*/
|
|
12
|
-
import { integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
12
|
+
import { index, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
13
13
|
import type { Actor } from '../../events/envelope'
|
|
14
14
|
import type { TicketRef } from '../../ontology'
|
|
15
|
+
import type { TicketAssetEntry } from '../ticket-assets'
|
|
15
16
|
import type { StreamPart } from '../streams/types'
|
|
16
17
|
|
|
17
18
|
export const builds = sqliteTable('builds', {
|
|
@@ -39,7 +40,12 @@ export const events = sqliteTable(
|
|
|
39
40
|
type: text('type').notNull(),
|
|
40
41
|
payload: text('payload', { mode: 'json' }).notNull(),
|
|
41
42
|
},
|
|
42
|
-
|
|
43
|
+
// Mirrors the BOOTSTRAP_DDL digest-scan index (AUT-487), type-leading so the
|
|
44
|
+
// digest query's cost grows with digest-relevant events, not total history.
|
|
45
|
+
(t) => [
|
|
46
|
+
primaryKey({ columns: [t.build, t.seq] }),
|
|
47
|
+
index('events_type_build_seq').on(t.type, t.build, t.seq),
|
|
48
|
+
],
|
|
43
49
|
)
|
|
44
50
|
|
|
45
51
|
export const repoStreams = sqliteTable('repo_streams', {
|
|
@@ -78,6 +84,27 @@ export const repoArtifacts = sqliteTable(
|
|
|
78
84
|
(t) => [primaryKey({ columns: [t.repo, t.kind, t.revision] })],
|
|
79
85
|
)
|
|
80
86
|
|
|
87
|
+
/** Ticket assets (SPEC §7.1): one row per revision, manifest as JSON, file
|
|
88
|
+
* bytes in the BlobStore. A removal is a tombstone row (`removed`, empty
|
|
89
|
+
* manifest) taking the next revision. No FK to `repo_streams`: assets can
|
|
90
|
+
* precede any repo-journal row. */
|
|
91
|
+
export const ticketAssets = sqliteTable(
|
|
92
|
+
'ticket_assets',
|
|
93
|
+
{
|
|
94
|
+
repo: text('repo').notNull(),
|
|
95
|
+
ticketId: text('ticket_id').notNull(),
|
|
96
|
+
kind: text('kind').notNull(),
|
|
97
|
+
name: text('name').notNull(),
|
|
98
|
+
revision: integer('revision').notNull(),
|
|
99
|
+
layout: text('layout').notNull().$type<'file' | 'tree'>(),
|
|
100
|
+
manifest: text('manifest', { mode: 'json' }).notNull().$type<TicketAssetEntry[]>(),
|
|
101
|
+
size: integer('size').notNull(),
|
|
102
|
+
removed: integer('removed').notNull(),
|
|
103
|
+
createdAt: text('created_at').notNull(),
|
|
104
|
+
},
|
|
105
|
+
(t) => [primaryKey({ columns: [t.repo, t.ticketId, t.kind, t.name, t.revision] })],
|
|
106
|
+
)
|
|
107
|
+
|
|
81
108
|
/** Operator sessions (SPEC §7.1.1): hosted-only durable orchestrator
|
|
82
109
|
* conversation state. Adapters implement the contract uniformly; nothing
|
|
83
110
|
* local creates one. */
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* content-addressed.
|
|
12
12
|
*/
|
|
13
13
|
import { Database } from 'bun:sqlite'
|
|
14
|
-
import { and, asc, desc, eq, gt, inArray, sql } from 'drizzle-orm'
|
|
14
|
+
import { and, asc, desc, eq, gt, gte, inArray, or, sql } from 'drizzle-orm'
|
|
15
15
|
import { drizzle, type BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite'
|
|
16
16
|
import { mkdirSync } from 'node:fs'
|
|
17
17
|
import { access, copyFile, mkdtemp, rm } from 'node:fs/promises'
|
|
@@ -40,7 +40,13 @@ import {
|
|
|
40
40
|
type SessionEventWrite,
|
|
41
41
|
} from '../../events/sessions'
|
|
42
42
|
import { createBuildScopedStore } from '../build-scope'
|
|
43
|
+
import { validateCreatedEvent } from '../new-build'
|
|
43
44
|
import { createSessionScopedStore } from '../session-handle'
|
|
45
|
+
import { DIGEST_EVENT_TYPES, reduceBuildDigest } from '../digest'
|
|
46
|
+
import {
|
|
47
|
+
REPOSITORY_STATE_EVENT_TYPES,
|
|
48
|
+
readRepoStateEventsWithAnchorRecheck,
|
|
49
|
+
} from '../repo-state-events'
|
|
44
50
|
import {
|
|
45
51
|
DEFAULT_ARTIFACT_RETENTION_MAX_REVISIONS,
|
|
46
52
|
isRetentionManagedKind,
|
|
@@ -49,7 +55,7 @@ import {
|
|
|
49
55
|
import { pollingSubscribe } from '../subscribe'
|
|
50
56
|
import { StreamLocks } from '../streams/lock'
|
|
51
57
|
import { assembleUIMessageDocument } from '../streams/assemble'
|
|
52
|
-
import { readEventsWithWait, readStreamWithWait } from '../streams/wait'
|
|
58
|
+
import { EVENT_WAIT_POLL_MS, readEventsWithWait, readStreamWithWait } from '../streams/wait'
|
|
53
59
|
import {
|
|
54
60
|
serializedBatchSize,
|
|
55
61
|
STREAM_BATCH_MAX_BYTES,
|
|
@@ -76,6 +82,7 @@ import {
|
|
|
76
82
|
type ArtifactMeta,
|
|
77
83
|
type BlobStore,
|
|
78
84
|
type BuildRecord,
|
|
85
|
+
type BuildDigest,
|
|
79
86
|
type BuildScopedStore,
|
|
80
87
|
type BuildStore,
|
|
81
88
|
type Clock,
|
|
@@ -91,6 +98,19 @@ import {
|
|
|
91
98
|
type SubscribeOptions,
|
|
92
99
|
type Unsubscribe,
|
|
93
100
|
} from '../types'
|
|
101
|
+
import {
|
|
102
|
+
DEFAULT_TICKET_ASSET_LIMITS,
|
|
103
|
+
loadTicketAsset,
|
|
104
|
+
storeTicketAssetBlobs,
|
|
105
|
+
resolvePinnedAsset,
|
|
106
|
+
summarizeTicketAsset,
|
|
107
|
+
validateTicketAssetInput,
|
|
108
|
+
type TicketAsset,
|
|
109
|
+
type TicketAssetInput,
|
|
110
|
+
type TicketAssetLimits,
|
|
111
|
+
type TicketAssetMeta,
|
|
112
|
+
type TicketAssetSummary,
|
|
113
|
+
} from '../ticket-assets'
|
|
94
114
|
import { DirBlobStore } from './blobs'
|
|
95
115
|
import {
|
|
96
116
|
artifacts,
|
|
@@ -102,6 +122,7 @@ import {
|
|
|
102
122
|
sessionArtifacts,
|
|
103
123
|
sessionEvents,
|
|
104
124
|
sessions,
|
|
125
|
+
ticketAssets,
|
|
105
126
|
streamChunks,
|
|
106
127
|
streams,
|
|
107
128
|
} from './schema'
|
|
@@ -111,7 +132,9 @@ import {
|
|
|
111
132
|
* the drizzle schema is the source of truth; this is its inlined form so
|
|
112
133
|
* opening a store never needs a migration step.
|
|
113
134
|
*/
|
|
114
|
-
|
|
135
|
+
// Exported for the schema-parity test, which pins this DDL against the
|
|
136
|
+
// drizzle schema; schema.ts stays the source of truth, this is its inlined form.
|
|
137
|
+
export const BOOTSTRAP_DDL = [
|
|
115
138
|
`CREATE TABLE IF NOT EXISTS builds (
|
|
116
139
|
slug TEXT PRIMARY KEY,
|
|
117
140
|
repo TEXT NOT NULL,
|
|
@@ -134,6 +157,10 @@ const BOOTSTRAP_DDL = [
|
|
|
134
157
|
payload TEXT NOT NULL,
|
|
135
158
|
PRIMARY KEY (build, seq)
|
|
136
159
|
)`,
|
|
160
|
+
// The build-digest scan (AUT-487): type-leading so the digest query's cost
|
|
161
|
+
// grows with the observation/terminal events themselves, not with total
|
|
162
|
+
// history. Idempotent at open, so pre-existing databases gain it too.
|
|
163
|
+
`CREATE INDEX IF NOT EXISTS events_type_build_seq ON events (type, build, seq)`,
|
|
137
164
|
`CREATE TABLE IF NOT EXISTS artifacts (
|
|
138
165
|
build TEXT NOT NULL,
|
|
139
166
|
kind TEXT NOT NULL,
|
|
@@ -170,6 +197,19 @@ const BOOTSTRAP_DDL = [
|
|
|
170
197
|
created_at TEXT NOT NULL,
|
|
171
198
|
PRIMARY KEY (repo, kind, revision)
|
|
172
199
|
)`,
|
|
200
|
+
`CREATE TABLE IF NOT EXISTS ticket_assets (
|
|
201
|
+
repo TEXT NOT NULL,
|
|
202
|
+
ticket_id TEXT NOT NULL,
|
|
203
|
+
kind TEXT NOT NULL,
|
|
204
|
+
name TEXT NOT NULL,
|
|
205
|
+
revision INTEGER NOT NULL,
|
|
206
|
+
layout TEXT NOT NULL,
|
|
207
|
+
manifest TEXT NOT NULL,
|
|
208
|
+
size INTEGER NOT NULL,
|
|
209
|
+
removed INTEGER NOT NULL,
|
|
210
|
+
created_at TEXT NOT NULL,
|
|
211
|
+
PRIMARY KEY (repo, ticket_id, kind, name, revision)
|
|
212
|
+
)`,
|
|
173
213
|
`CREATE TABLE IF NOT EXISTS sessions (
|
|
174
214
|
id TEXT PRIMARY KEY,
|
|
175
215
|
repo TEXT NOT NULL,
|
|
@@ -248,6 +288,15 @@ export interface SqliteBuildStoreOptions {
|
|
|
248
288
|
retention?: { maxRevisions?: number }
|
|
249
289
|
}
|
|
250
290
|
|
|
291
|
+
function ticketAssetKey(repo: string, ticketId: string, kind: string, name: string) {
|
|
292
|
+
return and(
|
|
293
|
+
eq(ticketAssets.repo, repo),
|
|
294
|
+
eq(ticketAssets.ticketId, ticketId),
|
|
295
|
+
eq(ticketAssets.kind, kind),
|
|
296
|
+
eq(ticketAssets.name, name),
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
|
|
251
300
|
export class SqliteBuildStore implements BuildStore {
|
|
252
301
|
private readonly sqlite: Database
|
|
253
302
|
private readonly db: BunSQLiteDatabase
|
|
@@ -399,6 +448,7 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
399
448
|
}
|
|
400
449
|
|
|
401
450
|
async createBuild(input: NewBuildInput): Promise<BuildRecord> {
|
|
451
|
+
const created = validateCreatedEvent(input)
|
|
402
452
|
const ts = this.now()
|
|
403
453
|
return this.writeTx(() => {
|
|
404
454
|
if (this.buildRow(input.slug)) {
|
|
@@ -416,6 +466,7 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
416
466
|
updatedAt: ts,
|
|
417
467
|
})
|
|
418
468
|
.run()
|
|
469
|
+
if (created !== undefined) this.appendInTx(input.slug, created)
|
|
419
470
|
return this.toRecord(this.requireBuild(input.slug))
|
|
420
471
|
})
|
|
421
472
|
}
|
|
@@ -430,6 +481,47 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
430
481
|
return rows.map((row) => this.toRecord(row))
|
|
431
482
|
}
|
|
432
483
|
|
|
484
|
+
async getRepoBuildDigests(repo: string): Promise<Map<string, BuildDigest>> {
|
|
485
|
+
// From the builds side (AUT-487): the type filter lives in the ON clause
|
|
486
|
+
// so a build whose log holds none of the digest-relevant event
|
|
487
|
+
// types still yields its row — a join that starts from `events` would
|
|
488
|
+
// silently drop such builds and break the operation's completeness
|
|
489
|
+
// contract. Only the digest-relevant types are fetched, then one shared
|
|
490
|
+
// derivation
|
|
491
|
+
// per build, so the answer cannot drift from `reduceBuild`.
|
|
492
|
+
const rows = this.sqlite
|
|
493
|
+
.query(
|
|
494
|
+
`SELECT b.slug AS slug, e.seq AS seq, e.ts AS ts, e.type AS type
|
|
495
|
+
FROM builds b
|
|
496
|
+
LEFT JOIN events e
|
|
497
|
+
ON e.build = b.slug AND e.type IN (${DIGEST_EVENT_TYPES.map(() => '?').join(', ')})
|
|
498
|
+
WHERE b.repo = ?
|
|
499
|
+
ORDER BY b.slug, e.seq`,
|
|
500
|
+
)
|
|
501
|
+
.all(...DIGEST_EVENT_TYPES, repo) as {
|
|
502
|
+
slug: string
|
|
503
|
+
seq: number | null
|
|
504
|
+
ts: string | null
|
|
505
|
+
type: string | null
|
|
506
|
+
}[]
|
|
507
|
+
const eventsByBuild = new Map<string, Pick<AbEvent, 'type' | 'seq' | 'ts'>[]>()
|
|
508
|
+
for (const row of rows) {
|
|
509
|
+
if (row.seq === null || row.ts === null || row.type === null) continue
|
|
510
|
+
const events = eventsByBuild.get(row.slug) ?? []
|
|
511
|
+
events.push({ type: row.type as AbEvent['type'], seq: row.seq, ts: row.ts })
|
|
512
|
+
eventsByBuild.set(row.slug, events)
|
|
513
|
+
}
|
|
514
|
+
const digests = new Map<string, BuildDigest>()
|
|
515
|
+
for (const row of rows) {
|
|
516
|
+
if (digests.has(row.slug)) continue
|
|
517
|
+
digests.set(row.slug, {
|
|
518
|
+
slug: row.slug,
|
|
519
|
+
...reduceBuildDigest(eventsByBuild.get(row.slug) ?? []),
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
return digests
|
|
523
|
+
}
|
|
524
|
+
|
|
433
525
|
/** Current build-stream tail inside an open transaction. */
|
|
434
526
|
private currentSeqInTx(slug: string): number {
|
|
435
527
|
this.requireBuild(slug)
|
|
@@ -664,6 +756,10 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
664
756
|
},
|
|
665
757
|
waitSeconds: opts?.waitSeconds,
|
|
666
758
|
signal: opts?.signal,
|
|
759
|
+
// Hosted budget: held build-event reads poll at most once per second
|
|
760
|
+
// (AUT-394) — not the 25 ms stream default, which is presentation
|
|
761
|
+
// content's cadence.
|
|
762
|
+
pollMs: EVENT_WAIT_POLL_MS,
|
|
667
763
|
})
|
|
668
764
|
}
|
|
669
765
|
|
|
@@ -935,9 +1031,81 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
935
1031
|
},
|
|
936
1032
|
waitSeconds: opts?.waitSeconds,
|
|
937
1033
|
signal: opts?.signal,
|
|
1034
|
+
// Hosted budget: held repository-event reads poll at most once per
|
|
1035
|
+
// second (AUT-394) — not the 25 ms stream default, which is
|
|
1036
|
+
// presentation content's cadence.
|
|
1037
|
+
pollMs: EVENT_WAIT_POLL_MS,
|
|
938
1038
|
})
|
|
939
1039
|
}
|
|
940
1040
|
|
|
1041
|
+
async getRepoStateEvents(repo: string): Promise<RepositoryEvent[]> {
|
|
1042
|
+
this.requireRepo(repo)
|
|
1043
|
+
// Two queries, mirroring the oracle's derivation: the latest
|
|
1044
|
+
// run-started anchor, then one select of durable types plus the tail
|
|
1045
|
+
// from that anchor. No new index: local journals are small and the
|
|
1046
|
+
// existing (repo, seq) PK index keeps the anchor scan acceptable.
|
|
1047
|
+
//
|
|
1048
|
+
// The two drizzle reads are synchronous, so there is no in-process await
|
|
1049
|
+
// point between them — but a concurrent writer from another process can
|
|
1050
|
+
// still land between the two statements in real time (AUT-551): a
|
|
1051
|
+
// first-ever `dispatcher.run-started` appended between them would be
|
|
1052
|
+
// invisible to the probe, absent from the durable-only selection, yet
|
|
1053
|
+
// present in the journal by the time this method returns. The shared
|
|
1054
|
+
// helper below closes that window with a one-shot anchor re-check on the
|
|
1055
|
+
// durable-only outcome: a durable-only result is returned only when the
|
|
1056
|
+
// journal was anchor-free as of the final probe. When the probe *does*
|
|
1057
|
+
// find an anchor, the anchored select's `gte(seq, anchor)` arm is
|
|
1058
|
+
// monotone-inclusive, so any later run-started is picked up by that same
|
|
1059
|
+
// select; an append after the final re-probe is an ordinary post-read
|
|
1060
|
+
// concurrent append, not this race.
|
|
1061
|
+
return readRepoStateEventsWithAnchorRecheck(
|
|
1062
|
+
async () => {
|
|
1063
|
+
// This probe is a LIMIT 1 select, not an aggregate: it returns zero or
|
|
1064
|
+
// one rows, so row absence is the deliberate empty-journal signal — no
|
|
1065
|
+
// MAX() NULL semantics involved (contrast the Postgres adapter's
|
|
1066
|
+
// MAX(seq) probe, which always returns one row and must test the NULL
|
|
1067
|
+
// explicitly). The `anchor === undefined` branch in the select below is
|
|
1068
|
+
// the explicit durable-only path; a null anchor must never reach the
|
|
1069
|
+
// gte(repoEvents.seq, anchor) arm (in either dialect the comparison is
|
|
1070
|
+
// never true, so the durable-only outcome would then survive only by
|
|
1071
|
+
// accident), and the no-anchor case must not degenerate into
|
|
1072
|
+
// gte(repoEvents.seq, 0), which would select the whole journal.
|
|
1073
|
+
const anchorRows = this.db
|
|
1074
|
+
.select({ seq: repoEvents.seq })
|
|
1075
|
+
.from(repoEvents)
|
|
1076
|
+
.where(and(eq(repoEvents.repo, repo), eq(repoEvents.type, 'dispatcher.run-started')))
|
|
1077
|
+
.orderBy(desc(repoEvents.seq))
|
|
1078
|
+
.limit(1)
|
|
1079
|
+
.all()
|
|
1080
|
+
return anchorRows[0]?.seq
|
|
1081
|
+
},
|
|
1082
|
+
async (anchor) => {
|
|
1083
|
+
const durable = inArray(repoEvents.type, [...REPOSITORY_STATE_EVENT_TYPES])
|
|
1084
|
+
const rows = this.db
|
|
1085
|
+
.select()
|
|
1086
|
+
.from(repoEvents)
|
|
1087
|
+
.where(
|
|
1088
|
+
anchor === undefined
|
|
1089
|
+
? and(eq(repoEvents.repo, repo), durable)
|
|
1090
|
+
: and(eq(repoEvents.repo, repo), or(gte(repoEvents.seq, anchor), durable)),
|
|
1091
|
+
)
|
|
1092
|
+
.orderBy(asc(repoEvents.seq))
|
|
1093
|
+
.all()
|
|
1094
|
+
return rows.map(
|
|
1095
|
+
(row) =>
|
|
1096
|
+
({
|
|
1097
|
+
repo: row.repo,
|
|
1098
|
+
seq: row.seq,
|
|
1099
|
+
ts: row.ts,
|
|
1100
|
+
actor: row.actor,
|
|
1101
|
+
type: row.type,
|
|
1102
|
+
payload: row.payload,
|
|
1103
|
+
}) as RepositoryEvent,
|
|
1104
|
+
)
|
|
1105
|
+
},
|
|
1106
|
+
)
|
|
1107
|
+
}
|
|
1108
|
+
|
|
941
1109
|
async putRepoArtifact(repo: string, artifact: ArtifactInput): Promise<RepositoryArtifactMeta> {
|
|
942
1110
|
const prepared = await this.prepareArtifact(artifact)
|
|
943
1111
|
return this.writeTx(() => this.depositRepoInTx(repo, prepared))
|
|
@@ -994,6 +1162,144 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
994
1162
|
.map((row) => this.toRepoMeta(row))
|
|
995
1163
|
}
|
|
996
1164
|
|
|
1165
|
+
async ticketAssetLimits(_repo: string): Promise<TicketAssetLimits> {
|
|
1166
|
+
return { ...DEFAULT_TICKET_ASSET_LIMITS }
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
private toTicketAssetMeta(row: typeof ticketAssets.$inferSelect): TicketAssetMeta {
|
|
1170
|
+
return {
|
|
1171
|
+
repo: row.repo,
|
|
1172
|
+
ticketId: row.ticketId,
|
|
1173
|
+
kind: row.kind,
|
|
1174
|
+
name: row.name,
|
|
1175
|
+
revision: row.revision,
|
|
1176
|
+
layout: row.layout,
|
|
1177
|
+
size: row.size,
|
|
1178
|
+
entries: row.manifest,
|
|
1179
|
+
createdAt: row.createdAt,
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
async putTicketAsset(
|
|
1184
|
+
repo: string,
|
|
1185
|
+
ticketId: string,
|
|
1186
|
+
asset: TicketAssetInput,
|
|
1187
|
+
): Promise<TicketAssetMeta> {
|
|
1188
|
+
const size = validateTicketAssetInput(asset)
|
|
1189
|
+
const entries = await storeTicketAssetBlobs(this.blobs, asset.entries)
|
|
1190
|
+
return this.writeTx(() => {
|
|
1191
|
+
const latest = this.db
|
|
1192
|
+
.select({ revision: ticketAssets.revision })
|
|
1193
|
+
.from(ticketAssets)
|
|
1194
|
+
.where(ticketAssetKey(repo, ticketId, asset.kind, asset.name))
|
|
1195
|
+
.orderBy(desc(ticketAssets.revision))
|
|
1196
|
+
.limit(1)
|
|
1197
|
+
.get()
|
|
1198
|
+
const row: typeof ticketAssets.$inferInsert = {
|
|
1199
|
+
repo,
|
|
1200
|
+
ticketId,
|
|
1201
|
+
kind: asset.kind,
|
|
1202
|
+
name: asset.name,
|
|
1203
|
+
revision: (latest?.revision ?? -1) + 1,
|
|
1204
|
+
layout: asset.layout,
|
|
1205
|
+
manifest: entries,
|
|
1206
|
+
size,
|
|
1207
|
+
removed: 0,
|
|
1208
|
+
createdAt: this.clock().toISOString(),
|
|
1209
|
+
}
|
|
1210
|
+
this.db.insert(ticketAssets).values(row).run()
|
|
1211
|
+
return this.toTicketAssetMeta(row as typeof ticketAssets.$inferSelect)
|
|
1212
|
+
})
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
async getTicketAsset(
|
|
1216
|
+
repo: string,
|
|
1217
|
+
ticketId: string,
|
|
1218
|
+
kind: string,
|
|
1219
|
+
name: string,
|
|
1220
|
+
rev?: number,
|
|
1221
|
+
): Promise<TicketAsset | null> {
|
|
1222
|
+
const scoped = ticketAssetKey(repo, ticketId, kind, name)
|
|
1223
|
+
const row =
|
|
1224
|
+
rev === undefined
|
|
1225
|
+
? this.db
|
|
1226
|
+
.select()
|
|
1227
|
+
.from(ticketAssets)
|
|
1228
|
+
.where(scoped)
|
|
1229
|
+
.orderBy(desc(ticketAssets.revision))
|
|
1230
|
+
.limit(1)
|
|
1231
|
+
.get()
|
|
1232
|
+
: this.db
|
|
1233
|
+
.select()
|
|
1234
|
+
.from(ticketAssets)
|
|
1235
|
+
.where(and(scoped, eq(ticketAssets.revision, rev)))
|
|
1236
|
+
.get()
|
|
1237
|
+
if (!row || row.removed) return null
|
|
1238
|
+
return loadTicketAsset(this.blobs, this.toTicketAssetMeta(row))
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
async getPinnedTicketAsset(
|
|
1242
|
+
slug: string,
|
|
1243
|
+
kind: string,
|
|
1244
|
+
name: string,
|
|
1245
|
+
rev?: number,
|
|
1246
|
+
): Promise<TicketAsset | null> {
|
|
1247
|
+
return resolvePinnedAsset(this, slug, kind, name, rev)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
async listTicketAssets(
|
|
1251
|
+
repo: string,
|
|
1252
|
+
ticketId: string,
|
|
1253
|
+
opts?: { revisions?: boolean },
|
|
1254
|
+
): Promise<TicketAssetSummary[]> {
|
|
1255
|
+
const rows = this.db
|
|
1256
|
+
.select()
|
|
1257
|
+
.from(ticketAssets)
|
|
1258
|
+
.where(and(eq(ticketAssets.repo, repo), eq(ticketAssets.ticketId, ticketId)))
|
|
1259
|
+
.orderBy(asc(ticketAssets.kind), asc(ticketAssets.name), asc(ticketAssets.revision))
|
|
1260
|
+
.all()
|
|
1261
|
+
if (opts?.revisions) {
|
|
1262
|
+
return rows
|
|
1263
|
+
.filter((row) => !row.removed)
|
|
1264
|
+
.map((row) => summarizeTicketAsset(this.toTicketAssetMeta(row)))
|
|
1265
|
+
}
|
|
1266
|
+
const latest = new Map<string, typeof ticketAssets.$inferSelect>()
|
|
1267
|
+
for (const row of rows) latest.set(JSON.stringify([row.kind, row.name]), row)
|
|
1268
|
+
return [...latest.values()]
|
|
1269
|
+
.filter((row) => !row.removed)
|
|
1270
|
+
.map((row) => summarizeTicketAsset(this.toTicketAssetMeta(row)))
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
async removeTicketAsset(
|
|
1274
|
+
repo: string,
|
|
1275
|
+
ticketId: string,
|
|
1276
|
+
kind: string,
|
|
1277
|
+
name: string,
|
|
1278
|
+
): Promise<TicketAssetMeta | null> {
|
|
1279
|
+
return this.writeTx(() => {
|
|
1280
|
+
const latest = this.db
|
|
1281
|
+
.select()
|
|
1282
|
+
.from(ticketAssets)
|
|
1283
|
+
.where(ticketAssetKey(repo, ticketId, kind, name))
|
|
1284
|
+
.orderBy(desc(ticketAssets.revision))
|
|
1285
|
+
.limit(1)
|
|
1286
|
+
.get()
|
|
1287
|
+
if (!latest || latest.removed) return null
|
|
1288
|
+
this.db
|
|
1289
|
+
.insert(ticketAssets)
|
|
1290
|
+
.values({
|
|
1291
|
+
...latest,
|
|
1292
|
+
revision: latest.revision + 1,
|
|
1293
|
+
manifest: [],
|
|
1294
|
+
size: 0,
|
|
1295
|
+
removed: 1,
|
|
1296
|
+
createdAt: this.clock().toISOString(),
|
|
1297
|
+
})
|
|
1298
|
+
.run()
|
|
1299
|
+
return this.toTicketAssetMeta(latest)
|
|
1300
|
+
})
|
|
1301
|
+
}
|
|
1302
|
+
|
|
997
1303
|
async claimRepoLease(repo: string, holder: string, ttlMs: number): Promise<boolean> {
|
|
998
1304
|
return this.writeTx(() => {
|
|
999
1305
|
const row = this.requireRepo(repo)
|
|
@@ -1189,6 +1495,26 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
1189
1495
|
return this.writeTx(() => this.appendSessionInTx(id, validated) as SessionEventEnvelope<T>)
|
|
1190
1496
|
}
|
|
1191
1497
|
|
|
1498
|
+
async appendSessionEventIfCurrent<T extends SessionEventType>(
|
|
1499
|
+
id: string,
|
|
1500
|
+
expectedSeq: number,
|
|
1501
|
+
event: SessionEventWrite<T>,
|
|
1502
|
+
): Promise<SessionEventEnvelope<T> | null> {
|
|
1503
|
+
validateExpectedSeq(expectedSeq)
|
|
1504
|
+
const validated = validateSessionEventWrite(event)
|
|
1505
|
+
return this.writeTx(() => {
|
|
1506
|
+
// BEGIN IMMEDIATE serializes this comparison and append across every
|
|
1507
|
+
// SQLite connection/process using the same store file.
|
|
1508
|
+
const row = this.db
|
|
1509
|
+
.select({ count: sql<number>`count(*)` })
|
|
1510
|
+
.from(sessionEvents)
|
|
1511
|
+
.where(eq(sessionEvents.session, id))
|
|
1512
|
+
.get()
|
|
1513
|
+
if ((row?.count ?? 0) !== expectedSeq) return null
|
|
1514
|
+
return this.appendSessionInTx(id, validated) as SessionEventEnvelope<T>
|
|
1515
|
+
})
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1192
1518
|
async getSessionEvents(
|
|
1193
1519
|
id: string,
|
|
1194
1520
|
sinceSeq = 0,
|
|
@@ -1214,7 +1540,15 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
1214
1540
|
}) as SessionEvent,
|
|
1215
1541
|
)
|
|
1216
1542
|
}
|
|
1217
|
-
return readEventsWithWait({
|
|
1543
|
+
return readEventsWithWait({
|
|
1544
|
+
read,
|
|
1545
|
+
waitSeconds: opts?.waitSeconds,
|
|
1546
|
+
signal: opts?.signal,
|
|
1547
|
+
// Hosted budget: held session-event reads poll at most once per second
|
|
1548
|
+
// (AUT-394), matching the build and repository event reads — not the
|
|
1549
|
+
// 25 ms stream default, which is presentation content's cadence.
|
|
1550
|
+
pollMs: EVENT_WAIT_POLL_MS,
|
|
1551
|
+
})
|
|
1218
1552
|
}
|
|
1219
1553
|
|
|
1220
1554
|
/** Runs inside an open transaction — see `depositInTx` for `prune`. */
|