@defrex/autobuild 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SPEC.md +138 -16
- package/bin/ab-dispatch-kernel.ts +4 -6
- package/docs/architecture.md +155 -73
- package/docs/configuration.md +41 -9
- package/docs/hosted-service.md +31 -5
- package/docs/operator-api.md +8 -1
- package/docs/remote-store-protocol.md +94 -2
- package/docs/remote-ticket-protocol.md +151 -9
- package/docs/setup.md +33 -7
- package/docs/ticket-migration.md +64 -0
- package/package.json +4 -3
- package/packages/core/src/cli/attention.ts +18 -164
- package/packages/core/src/cli/build-control.ts +23 -16
- package/packages/core/src/cli/bulk-control.ts +3 -2
- package/packages/core/src/cli/connect.ts +141 -38
- package/packages/core/src/cli/context.ts +23 -79
- package/packages/core/src/cli/dashboard/model.ts +36 -22
- package/packages/core/src/cli/dashboard/poll.ts +1 -1
- package/packages/core/src/cli/dispatch-frontend.ts +24 -67
- package/packages/core/src/cli/dispatch-process.ts +14 -7
- package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
- package/packages/core/src/cli/dispatch.ts +304 -82
- package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
- package/packages/core/src/cli/harvest.ts +84 -42
- package/packages/core/src/cli/help.ts +14 -8
- package/packages/core/src/cli/historical-build-state.ts +27 -0
- package/packages/core/src/cli/installation.ts +67 -13
- package/packages/core/src/cli/main.ts +23 -5
- package/packages/core/src/cli/mcp.ts +2 -1
- package/packages/core/src/cli/pr-attachments.ts +10 -13
- package/packages/core/src/cli/pr-summary-publication.ts +27 -60
- package/packages/core/src/cli/pr-summary.ts +43 -13
- package/packages/core/src/cli/publication-settlement.ts +10 -20
- package/packages/core/src/cli/repository-status.ts +38 -2
- package/packages/core/src/cli/self-update.ts +5 -1
- package/packages/core/src/cli/status.ts +62 -32
- package/packages/core/src/cli/terminals.ts +25 -142
- package/packages/core/src/cli/ticket-assets.ts +7 -2
- package/packages/core/src/cli/ticket-editor.ts +82 -0
- package/packages/core/src/cli/ticket.ts +166 -36
- package/packages/core/src/cli/wait.ts +91 -84
- package/packages/core/src/cli/watch.ts +60 -97
- package/packages/core/src/config/pipeline-source.ts +4 -3
- package/packages/core/src/config/schema.ts +35 -3
- package/packages/core/src/connection.ts +3 -3
- package/packages/core/src/events/catalog.ts +3 -2
- package/packages/core/src/events/payloads.ts +6 -0
- package/packages/core/src/events/repository.ts +7 -3
- package/packages/core/src/integration/harness.ts +38 -4
- package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2444 -9
- package/packages/core/src/kernel/auto-merge.ts +19 -3
- package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
- package/packages/core/src/kernel/generators/build-log.ts +1 -1
- package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
- package/packages/core/src/kernel/harvest.ts +25 -23
- package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
- package/packages/core/src/kernel/log-index-value.ts +38 -0
- package/packages/core/src/kernel/read-facts.ts +637 -0
- package/packages/core/src/kernel/reducer-registry.ts +139 -3
- package/packages/core/src/kernel/repository-attention.ts +179 -0
- package/packages/core/src/kernel/service-facts.ts +135 -0
- package/packages/core/src/kernel/terminal-guard.ts +133 -0
- package/packages/core/src/ontology.ts +1 -0
- package/packages/core/src/operator/annotations.ts +12 -0
- package/packages/core/src/operator/control.ts +5 -6
- package/packages/core/src/operator/index.ts +7 -0
- package/packages/core/src/operator/protocol.ts +19 -2
- package/packages/core/src/operator/query.ts +37 -20
- package/packages/core/src/operator/read-view.ts +4 -13
- package/packages/core/src/operator/registry.ts +61 -8
- package/packages/core/src/operator/requests.ts +2 -14
- package/packages/core/src/operator/sandbox.ts +10 -9
- package/packages/core/src/operator/ticket-source-backend.ts +17 -21
- package/packages/core/src/operator/tickets.ts +132 -22
- package/packages/core/src/plugin-sdk/index.ts +19 -0
- package/packages/core/src/plugins/runtimes.ts +5 -1
- package/packages/core/src/ports/runner/session-env.ts +2 -0
- package/packages/core/src/ports/tickets/contract.ts +66 -1
- package/packages/core/src/ports/tickets/create.ts +48 -12
- package/packages/core/src/ports/tickets/fake.ts +30 -1
- package/packages/core/src/ports/tickets/file-assets.ts +159 -0
- package/packages/core/src/ports/tickets/file-codec.ts +90 -0
- package/packages/core/src/ports/tickets/file.ts +114 -120
- package/packages/core/src/ports/tickets/grants.ts +47 -0
- package/packages/core/src/ports/tickets/hosted-index.ts +17 -0
- package/packages/core/src/ports/tickets/identity.ts +20 -0
- package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
- package/packages/core/src/ports/tickets/linear.ts +74 -9
- package/packages/core/src/ports/tickets/migration.ts +336 -0
- package/packages/core/src/ports/tickets/operations.ts +38 -7
- package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
- package/packages/core/src/ports/tickets/remote-protocol.ts +95 -0
- package/packages/core/src/ports/tickets/remote.ts +60 -3
- package/packages/core/src/ports/tickets/requested-id.ts +46 -0
- package/packages/core/src/ports/tickets/revision.ts +62 -0
- package/packages/core/src/ports/tickets/states.ts +61 -0
- package/packages/core/src/ports/types.ts +40 -3
- package/packages/core/src/ports/workspace/build-execution.ts +1 -0
- package/packages/core/src/ports/workspace/create.ts +5 -1
- package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
- package/packages/core/src/ports/workspace/vercel-sandbox.ts +8 -4
- package/packages/core/src/processes/build-child.ts +6 -6
- package/packages/core/src/processes/build-runner.ts +62 -70
- package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
- package/packages/core/src/processes/dispatcher.ts +223 -205
- package/packages/core/src/processes/execution-no-progress.ts +176 -0
- package/packages/core/src/processes/harvest-child.ts +30 -4
- package/packages/core/src/processes/harvest-execution-state.ts +15 -9
- package/packages/core/src/processes/harvest-runner.ts +58 -74
- package/packages/core/src/processes/harvest.ts +72 -29
- package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
- package/packages/core/src/processes/orchestrator-tick.ts +2 -2
- package/packages/core/src/processes/phase-failure-budget.ts +1 -0
- package/packages/core/src/processes/publication-loss.ts +35 -21
- package/packages/core/src/processes/repo-view.ts +229 -1099
- package/packages/core/src/processes/tick-publication.ts +110 -0
- package/packages/core/src/store/adapter.ts +23 -0
- package/packages/core/src/store/build-scope.ts +17 -0
- package/packages/core/src/store/contract.ts +283 -1
- package/packages/core/src/store/local/schema.ts +1 -0
- package/packages/core/src/store/local/store.ts +180 -55
- package/packages/core/src/store/memory.ts +119 -29
- package/packages/core/src/store/phase-session.ts +17 -0
- package/packages/core/src/store/reduced-reads.ts +239 -0
- package/packages/core/src/store/reducer-value-shapes.json +14056 -0
- package/packages/core/src/store/reducer-value-validation.ts +96 -0
- package/packages/core/src/store/reducer-values.ts +145 -0
- package/packages/core/src/store/remote/client.ts +41 -0
- package/packages/core/src/store/remote/compatibility.ts +2 -0
- package/packages/core/src/store/remote/index.ts +19 -0
- package/packages/core/src/store/remote/protocol.ts +16 -0
- package/packages/core/src/store/remote/version.ts +3 -1
- package/packages/core/src/store/repo-state-events.ts +1 -1
- package/packages/core/src/store/repo-tick.ts +72 -0
- package/packages/core/src/store/session-handle.ts +19 -0
- package/packages/core/src/store/snapshots.ts +10 -0
- package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
- package/packages/core/src/store/ticket-assets.ts +86 -8
- package/packages/core/src/store/types.ts +40 -3
- package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
- package/packages/core/src/testing/index.ts +8 -0
- package/packages/core/src/testing/reduced-read-budget.ts +462 -0
- package/skills/guide/SKILL.md +97 -12
- package/skills/guide/references/operator-api.md +8 -1
- package/skills/guide/references/remote-store-protocol.md +94 -2
- package/skills/guide/references/remote-ticket-protocol.md +151 -9
- package/skills/guide/references/setup.md +33 -7
- package/skills/guide/references/ticket-migration.md +64 -0
- package/skills/tickets/SKILL.md +71 -0
- package/templates/hosted-service/package.json +2 -2
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { readPersistedBuildDigests } from '../reduced-reads'
|
|
2
|
+
import { assertPublicSnapshotReducer } from '../snapshots'
|
|
3
|
+
import {
|
|
4
|
+
readReducerValue,
|
|
5
|
+
resolveReducerValue,
|
|
6
|
+
type ReducerValueRequest,
|
|
7
|
+
type ReducerValue,
|
|
8
|
+
} from '../reducer-values'
|
|
9
|
+
import { validateTickFinish } from '../repo-tick'
|
|
10
|
+
import type { RepoTickCompletion, RepoTickFinish, RepoTickObservation } from '../types'
|
|
1
11
|
/**
|
|
2
12
|
* The local BuildStore (SPEC §7.2.1): SQLite + blob directory beneath an
|
|
3
13
|
* explicitly resolved local state root. Repository/default selection belongs
|
|
@@ -43,7 +53,6 @@ import { createBuildScopedStore } from '../build-scope'
|
|
|
43
53
|
import type { ReducerSnapshot, SnapshotScope } from '../snapshots'
|
|
44
54
|
import { validateCreatedEvent } from '../new-build'
|
|
45
55
|
import { createSessionScopedStore } from '../session-handle'
|
|
46
|
-
import { DIGEST_EVENT_TYPES, type DigestEventRow, reduceBuildDigest } from '../digest'
|
|
47
56
|
import {
|
|
48
57
|
REPOSITORY_STATE_EVENT_TYPES,
|
|
49
58
|
REPOSITORY_LATEST_EVENT_TYPES,
|
|
@@ -191,7 +200,8 @@ export const BOOTSTRAP_DDL = [
|
|
|
191
200
|
lease_holder TEXT,
|
|
192
201
|
lease_expires_at TEXT,
|
|
193
202
|
lease_ttl_ms INTEGER,
|
|
194
|
-
heartbeat_at TEXT
|
|
203
|
+
heartbeat_at TEXT,
|
|
204
|
+
tick TEXT
|
|
195
205
|
)`,
|
|
196
206
|
`CREATE TABLE IF NOT EXISTS repo_events (
|
|
197
207
|
repo TEXT NOT NULL,
|
|
@@ -337,6 +347,11 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
337
347
|
// Stores created before `repo_origin` existed keep working: add the column
|
|
338
348
|
// idempotently when a pre-existing table lacks it (the bootstrap DDL above
|
|
339
349
|
// covers fresh stores). No migration framework — one guarded ALTER at open.
|
|
350
|
+
const repoColumns = this.sqlite.query("PRAGMA table_info('repo_streams')").all() as {
|
|
351
|
+
name: string
|
|
352
|
+
}[]
|
|
353
|
+
if (!repoColumns.some((column) => column.name === 'tick'))
|
|
354
|
+
this.sqlite.exec('ALTER TABLE repo_streams ADD COLUMN tick TEXT')
|
|
340
355
|
const columns = this.sqlite.query("PRAGMA table_info('builds')").all() as Array<{
|
|
341
356
|
name: string
|
|
342
357
|
}>
|
|
@@ -435,6 +450,7 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
435
450
|
|
|
436
451
|
private toRepoRecord(row: RepoRow): RepositoryRecord {
|
|
437
452
|
return {
|
|
453
|
+
...(row.tick ? { tick: structuredClone(row.tick) } : {}),
|
|
438
454
|
repo: row.repo,
|
|
439
455
|
createdAt: row.createdAt,
|
|
440
456
|
updatedAt: row.updatedAt,
|
|
@@ -496,56 +512,16 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
496
512
|
}
|
|
497
513
|
|
|
498
514
|
async getRepoBuildDigests(repo: string): Promise<Map<string, BuildDigest>> {
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
CASE WHEN e.type = 'pr-attachment.reclaimed'
|
|
510
|
-
THEN json_extract(e.payload, '$.hostedSeq') END AS hostedSeq,
|
|
511
|
-
CASE WHEN e.type IN ('execution.started', 'execution.ended')
|
|
512
|
-
THEN json_extract(e.payload, '$.instance') END AS instance
|
|
513
|
-
FROM builds b
|
|
514
|
-
LEFT JOIN events e
|
|
515
|
-
ON e.build = b.slug AND e.type IN (${DIGEST_EVENT_TYPES.map(() => '?').join(', ')})
|
|
516
|
-
WHERE b.repo = ?
|
|
517
|
-
ORDER BY b.slug, e.seq`,
|
|
518
|
-
)
|
|
519
|
-
.all(...DIGEST_EVENT_TYPES, repo) as {
|
|
520
|
-
slug: string
|
|
521
|
-
seq: number | null
|
|
522
|
-
ts: string | null
|
|
523
|
-
type: string | null
|
|
524
|
-
hostedSeq: number | null
|
|
525
|
-
instance: string | null
|
|
526
|
-
}[]
|
|
527
|
-
const eventsByBuild = new Map<string, DigestEventRow[]>()
|
|
528
|
-
for (const row of rows) {
|
|
529
|
-
if (row.seq === null || row.ts === null || row.type === null) continue
|
|
530
|
-
const events = eventsByBuild.get(row.slug) ?? []
|
|
531
|
-
events.push({
|
|
532
|
-
type: row.type as AbEvent['type'],
|
|
533
|
-
seq: row.seq,
|
|
534
|
-
ts: row.ts,
|
|
535
|
-
...(row.hostedSeq !== null ? { hostedSeq: row.hostedSeq } : {}),
|
|
536
|
-
...(row.instance !== null ? { instance: row.instance } : {}),
|
|
537
|
-
})
|
|
538
|
-
eventsByBuild.set(row.slug, events)
|
|
539
|
-
}
|
|
540
|
-
const digests = new Map<string, BuildDigest>()
|
|
541
|
-
for (const row of rows) {
|
|
542
|
-
if (digests.has(row.slug)) continue
|
|
543
|
-
digests.set(row.slug, {
|
|
544
|
-
slug: row.slug,
|
|
545
|
-
...reduceBuildDigest(eventsByBuild.get(row.slug) ?? []),
|
|
546
|
-
})
|
|
547
|
-
}
|
|
548
|
-
return digests
|
|
515
|
+
return readPersistedBuildDigests(
|
|
516
|
+
this,
|
|
517
|
+
repo,
|
|
518
|
+
this.db
|
|
519
|
+
.select()
|
|
520
|
+
.from(builds)
|
|
521
|
+
.where(eq(builds.repo, repo))
|
|
522
|
+
.all()
|
|
523
|
+
.map((row) => this.toRecord(row)),
|
|
524
|
+
)
|
|
549
525
|
}
|
|
550
526
|
|
|
551
527
|
/** Current build-stream tail inside an open transaction. */
|
|
@@ -922,9 +898,12 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
922
898
|
return row ? this.toRepoRecord(row) : null
|
|
923
899
|
}
|
|
924
900
|
|
|
925
|
-
private appendRepoInTx(
|
|
901
|
+
private appendRepoInTx(
|
|
902
|
+
repo: string,
|
|
903
|
+
validated: RepositoryEventWrite,
|
|
904
|
+
ts = this.now(),
|
|
905
|
+
): RepositoryEventEnvelope {
|
|
926
906
|
this.requireRepo(repo)
|
|
927
|
-
const ts = this.now()
|
|
928
907
|
const row = this.db
|
|
929
908
|
.select({ max: sql<number | null>`max(${repoEvents.seq})` })
|
|
930
909
|
.from(repoEvents)
|
|
@@ -1092,11 +1071,91 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
1092
1071
|
return row?.tail
|
|
1093
1072
|
}
|
|
1094
1073
|
|
|
1074
|
+
async getReducerValue(scope: SnapshotScope, request: ReducerValueRequest): Promise<ReducerValue> {
|
|
1075
|
+
// Reject invalid requests before touching storage.
|
|
1076
|
+
resolveReducerValue(scope, request)
|
|
1077
|
+
return readReducerValue(scope, request, {
|
|
1078
|
+
highWater: async () => {
|
|
1079
|
+
const tail = this.snapshotTail(scope)
|
|
1080
|
+
if (tail === undefined) throw new Error('unknown reducer scope')
|
|
1081
|
+
return tail
|
|
1082
|
+
},
|
|
1083
|
+
events: async (since, through) => {
|
|
1084
|
+
const key = scope.kind === 'build' ? scope.slug : scope.repo
|
|
1085
|
+
const rows = this.sqlite
|
|
1086
|
+
.query(
|
|
1087
|
+
scope.kind === 'build'
|
|
1088
|
+
? 'SELECT * FROM events WHERE build=?1 AND seq>?2 AND seq<=?3 ORDER BY seq'
|
|
1089
|
+
: 'SELECT * FROM repo_events WHERE repo=?1 AND seq>?2 AND seq<=?3 ORDER BY seq',
|
|
1090
|
+
)
|
|
1091
|
+
.all(key, since, through) as {
|
|
1092
|
+
build?: string
|
|
1093
|
+
repo?: string
|
|
1094
|
+
seq: number
|
|
1095
|
+
ts: string
|
|
1096
|
+
actor: string
|
|
1097
|
+
type: string
|
|
1098
|
+
payload: string
|
|
1099
|
+
}[]
|
|
1100
|
+
return rows.map((row) => ({
|
|
1101
|
+
...row,
|
|
1102
|
+
actor: JSON.parse(row.actor),
|
|
1103
|
+
payload: JSON.parse(row.payload),
|
|
1104
|
+
})) as (AbEvent | RepositoryEvent)[]
|
|
1105
|
+
},
|
|
1106
|
+
put: (key, snapshot) => this.putSnapshot(scope, key, snapshot),
|
|
1107
|
+
read: async (key) => {
|
|
1108
|
+
const row = this.sqlite
|
|
1109
|
+
.query(
|
|
1110
|
+
'SELECT version, cursor, state FROM reducer_snapshots WHERE scope_kind=?1 AND scope_key=?2 AND reducer=?3',
|
|
1111
|
+
)
|
|
1112
|
+
.get(scope.kind, scope.kind === 'build' ? scope.slug : scope.repo, key) as {
|
|
1113
|
+
version: number
|
|
1114
|
+
cursor: number
|
|
1115
|
+
state: string
|
|
1116
|
+
} | null
|
|
1117
|
+
if (!row) return null
|
|
1118
|
+
let state: unknown = null
|
|
1119
|
+
try {
|
|
1120
|
+
state = JSON.parse(row.state)
|
|
1121
|
+
} catch {
|
|
1122
|
+
/* malformed cache */
|
|
1123
|
+
}
|
|
1124
|
+
return { snapshot: { version: row.version, cursor: row.cursor, state }, token: row.state }
|
|
1125
|
+
},
|
|
1126
|
+
repair: async (key, observed, snapshot) => {
|
|
1127
|
+
return this.sqlite.transaction(() => {
|
|
1128
|
+
const tail = this.snapshotTail(scope)
|
|
1129
|
+
if (tail === undefined || snapshot.cursor > tail) return false
|
|
1130
|
+
return (
|
|
1131
|
+
this.sqlite
|
|
1132
|
+
.query(
|
|
1133
|
+
`UPDATE reducer_snapshots SET version=?1, cursor=?2, state=?3, updated_at=?4 WHERE scope_kind=?5 AND scope_key=?6 AND reducer=?7 AND version=?8 AND cursor=?9 AND state=?10`,
|
|
1134
|
+
)
|
|
1135
|
+
.run(
|
|
1136
|
+
snapshot.version,
|
|
1137
|
+
snapshot.cursor,
|
|
1138
|
+
JSON.stringify(snapshot.state),
|
|
1139
|
+
this.clock().toISOString(),
|
|
1140
|
+
scope.kind,
|
|
1141
|
+
scope.kind === 'build' ? scope.slug : scope.repo,
|
|
1142
|
+
key,
|
|
1143
|
+
observed.snapshot.version,
|
|
1144
|
+
observed.snapshot.cursor,
|
|
1145
|
+
observed.token,
|
|
1146
|
+
).changes > 0
|
|
1147
|
+
)
|
|
1148
|
+
})()
|
|
1149
|
+
},
|
|
1150
|
+
})
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1095
1153
|
async getReducerSnapshot(
|
|
1096
1154
|
scope: SnapshotScope,
|
|
1097
1155
|
reducer: string,
|
|
1098
1156
|
version: number,
|
|
1099
1157
|
): Promise<ReducerSnapshot | null> {
|
|
1158
|
+
assertPublicSnapshotReducer(reducer)
|
|
1100
1159
|
const tail = this.snapshotTail(scope)
|
|
1101
1160
|
if (tail === undefined) return null
|
|
1102
1161
|
const row = this.sqlite
|
|
@@ -1120,6 +1179,15 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
1120
1179
|
scope: SnapshotScope,
|
|
1121
1180
|
reducer: string,
|
|
1122
1181
|
snapshot: ReducerSnapshot,
|
|
1182
|
+
): Promise<boolean> {
|
|
1183
|
+
assertPublicSnapshotReducer(reducer)
|
|
1184
|
+
return this.putSnapshot(scope, reducer, snapshot)
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
private async putSnapshot(
|
|
1188
|
+
scope: SnapshotScope,
|
|
1189
|
+
reducer: string,
|
|
1190
|
+
snapshot: ReducerSnapshot,
|
|
1123
1191
|
): Promise<boolean> {
|
|
1124
1192
|
const tail = this.snapshotTail(scope)
|
|
1125
1193
|
if (tail === undefined || snapshot.cursor > tail) return false
|
|
@@ -1420,6 +1488,63 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
1420
1488
|
})
|
|
1421
1489
|
}
|
|
1422
1490
|
|
|
1491
|
+
async beginRepoTick(repo: string, holder: string): Promise<string | null> {
|
|
1492
|
+
return this.writeTx(() => {
|
|
1493
|
+
const row = this.requireRepo(repo)
|
|
1494
|
+
if (
|
|
1495
|
+
row.leaseHolder !== holder ||
|
|
1496
|
+
!row.leaseExpiresAt ||
|
|
1497
|
+
Date.parse(row.leaseExpiresAt) <= this.clock().getTime()
|
|
1498
|
+
)
|
|
1499
|
+
return null
|
|
1500
|
+
const startedAt = this.now()
|
|
1501
|
+
const tick: RepoTickObservation = {
|
|
1502
|
+
run: holder,
|
|
1503
|
+
open: { startedAt },
|
|
1504
|
+
...(row.tick?.finished ? { finished: row.tick.finished } : {}),
|
|
1505
|
+
}
|
|
1506
|
+
this.db
|
|
1507
|
+
.update(repoStreams)
|
|
1508
|
+
.set({ tick, updatedAt: startedAt })
|
|
1509
|
+
.where(eq(repoStreams.repo, repo))
|
|
1510
|
+
.run()
|
|
1511
|
+
return startedAt
|
|
1512
|
+
})
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
async finishRepoTick(
|
|
1516
|
+
repo: string,
|
|
1517
|
+
holder: string,
|
|
1518
|
+
completion: RepoTickCompletion,
|
|
1519
|
+
events: RepositoryEventWrite[],
|
|
1520
|
+
): Promise<RepoTickFinish | null> {
|
|
1521
|
+
const validated = validateTickFinish(holder, completion, events)
|
|
1522
|
+
return this.writeTx(() => {
|
|
1523
|
+
const row = this.requireRepo(repo)
|
|
1524
|
+
if (
|
|
1525
|
+
row.leaseHolder !== holder ||
|
|
1526
|
+
!row.leaseExpiresAt ||
|
|
1527
|
+
Date.parse(row.leaseExpiresAt) <= this.clock().getTime()
|
|
1528
|
+
)
|
|
1529
|
+
return null
|
|
1530
|
+
if (row.tick?.run !== holder || !row.tick.open) throw new Error('no open repository tick')
|
|
1531
|
+
const completedAt = this.now()
|
|
1532
|
+
const envelopes = validated.map((event) =>
|
|
1533
|
+
this.appendRepoInTx(repo, event, completedAt),
|
|
1534
|
+
) as RepositoryEvent[]
|
|
1535
|
+
const observation: RepoTickObservation = {
|
|
1536
|
+
run: holder,
|
|
1537
|
+
finished: { ...completion, run: holder, startedAt: row.tick.open.startedAt, completedAt },
|
|
1538
|
+
}
|
|
1539
|
+
this.db
|
|
1540
|
+
.update(repoStreams)
|
|
1541
|
+
.set({ tick: observation, updatedAt: completedAt })
|
|
1542
|
+
.where(eq(repoStreams.repo, repo))
|
|
1543
|
+
.run()
|
|
1544
|
+
return { observation, events: envelopes }
|
|
1545
|
+
})
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1423
1548
|
async claimRepoLease(repo: string, holder: string, ttlMs: number): Promise<boolean> {
|
|
1424
1549
|
return this.writeTx(() => {
|
|
1425
1550
|
const row = this.requireRepo(repo)
|
|
@@ -2036,7 +2161,7 @@ export class SqliteBuildStore implements BuildStore {
|
|
|
2036
2161
|
return { stale: false, record: this.toStreamRecord(this.requireStream(streamId)) }
|
|
2037
2162
|
},
|
|
2038
2163
|
)
|
|
2039
|
-
if (
|
|
2164
|
+
if (committed.stale === false) return committed.record
|
|
2040
2165
|
// A foreign appender landed inside the prepare window; loop and
|
|
2041
2166
|
// re-prepare from the newer chunk set so its acknowledged chunk is
|
|
2042
2167
|
// not omitted from the finalized artifact.
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
import { readPersistedBuildDigests } from './reduced-reads'
|
|
2
|
+
import { assertPublicSnapshotReducer } from './snapshots'
|
|
3
|
+
import {
|
|
4
|
+
readReducerValue,
|
|
5
|
+
resolveReducerValue,
|
|
6
|
+
type ReducerValueRequest,
|
|
7
|
+
type ReducerValue,
|
|
8
|
+
} from './reducer-values'
|
|
9
|
+
import { validateTickFinish } from './repo-tick'
|
|
10
|
+
import type { RepoTickCompletion, RepoTickFinish, RepoTickObservation } from './types'
|
|
1
11
|
/**
|
|
2
12
|
* The in-memory BuildStore — the reference adapter. Its behavior *is* the
|
|
3
13
|
* contract: the suite in `store/contract.ts` is written against this and
|
|
@@ -34,7 +44,6 @@ import {
|
|
|
34
44
|
} from './snapshots'
|
|
35
45
|
import { validateCreatedEvent } from './new-build'
|
|
36
46
|
import { createSessionScopedStore } from './session-handle'
|
|
37
|
-
import { reduceBuildDigest } from './digest'
|
|
38
47
|
import { projectRepositoryStateEvents } from './repo-state-events'
|
|
39
48
|
import {
|
|
40
49
|
DEFAULT_ARTIFACT_RETENTION_MAX_REVISIONS,
|
|
@@ -121,6 +130,7 @@ interface Lease {
|
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
interface RepoState {
|
|
133
|
+
tick?: RepoTickObservation
|
|
124
134
|
record: {
|
|
125
135
|
repo: string
|
|
126
136
|
createdAt: string
|
|
@@ -316,34 +326,7 @@ export class MemoryBuildStore implements BuildStore {
|
|
|
316
326
|
}
|
|
317
327
|
|
|
318
328
|
async getRepoBuildDigests(repo: string): Promise<Map<string, BuildDigest>> {
|
|
319
|
-
|
|
320
|
-
// digest map is returned in slug order so every adapter answers the same
|
|
321
|
-
// shape for the same store contents.
|
|
322
|
-
const slugs = [...this.builds.entries()]
|
|
323
|
-
.filter(([, state]) => state.record.repo === repo)
|
|
324
|
-
.map(([slug]) => slug)
|
|
325
|
-
.sort()
|
|
326
|
-
const digests = new Map<string, BuildDigest>()
|
|
327
|
-
for (const slug of slugs) {
|
|
328
|
-
const state = this.builds.get(slug)!
|
|
329
|
-
digests.set(slug, {
|
|
330
|
-
slug,
|
|
331
|
-
...reduceBuildDigest(
|
|
332
|
-
state.events.map((event) => ({
|
|
333
|
-
type: event.type,
|
|
334
|
-
seq: event.seq,
|
|
335
|
-
ts: event.ts,
|
|
336
|
-
...(event.type === 'pr-attachment.reclaimed'
|
|
337
|
-
? { hostedSeq: event.payload.hostedSeq }
|
|
338
|
-
: {}),
|
|
339
|
-
...(event.type === 'execution.started' || event.type === 'execution.ended'
|
|
340
|
-
? { instance: event.payload.instance }
|
|
341
|
-
: {}),
|
|
342
|
-
})),
|
|
343
|
-
),
|
|
344
|
-
})
|
|
345
|
-
}
|
|
346
|
-
return digests
|
|
329
|
+
return readPersistedBuildDigests(this, repo)
|
|
347
330
|
}
|
|
348
331
|
|
|
349
332
|
async append<T extends EventType>(slug: string, event: EventWrite<T>): Promise<EventEnvelope<T>> {
|
|
@@ -563,6 +546,7 @@ export class MemoryBuildStore implements BuildStore {
|
|
|
563
546
|
|
|
564
547
|
private repoSnapshot(state: RepoState): RepositoryRecord {
|
|
565
548
|
return {
|
|
549
|
+
...(state.tick ? { tick: structuredClone(state.tick) } : {}),
|
|
566
550
|
repo: state.record.repo,
|
|
567
551
|
createdAt: state.record.createdAt,
|
|
568
552
|
updatedAt: state.record.updatedAt,
|
|
@@ -714,11 +698,54 @@ export class MemoryBuildStore implements BuildStore {
|
|
|
714
698
|
return events === undefined ? undefined : (events.at(-1)?.seq ?? 0)
|
|
715
699
|
}
|
|
716
700
|
|
|
701
|
+
async getReducerValue(scope: SnapshotScope, request: ReducerValueRequest): Promise<ReducerValue> {
|
|
702
|
+
// Reject invalid requests before touching storage.
|
|
703
|
+
resolveReducerValue(scope, request)
|
|
704
|
+
return readReducerValue(scope, request, {
|
|
705
|
+
highWater: async () => {
|
|
706
|
+
const tail = this.snapshotTail(scope)
|
|
707
|
+
if (tail === undefined) throw new Error('unknown reducer scope')
|
|
708
|
+
return tail
|
|
709
|
+
},
|
|
710
|
+
events: async (since, through) =>
|
|
711
|
+
structuredClone(
|
|
712
|
+
(scope.kind === 'build'
|
|
713
|
+
? this.state(scope.slug).events
|
|
714
|
+
: this.repoState(scope.repo).events
|
|
715
|
+
).slice(since, through),
|
|
716
|
+
) as (AbEvent | RepositoryEvent)[],
|
|
717
|
+
put: (key, snapshot) => this.putSnapshot(scope, key, snapshot),
|
|
718
|
+
read: async (key) => {
|
|
719
|
+
const snapshot = this.snapshots.get(`${snapshotScopeKey(scope)}\u0000${key}`)
|
|
720
|
+
return snapshot === undefined
|
|
721
|
+
? null
|
|
722
|
+
: { snapshot: structuredClone(snapshot), token: JSON.stringify(snapshot) }
|
|
723
|
+
},
|
|
724
|
+
repair: async (key, observed, snapshot) => {
|
|
725
|
+
const cacheKey = `${snapshotScopeKey(scope)}\u0000${key}`
|
|
726
|
+
const current = this.snapshots.get(cacheKey)
|
|
727
|
+
const tail = this.snapshotTail(scope)
|
|
728
|
+
if (
|
|
729
|
+
!current ||
|
|
730
|
+
JSON.stringify(current) !== observed.token ||
|
|
731
|
+
tail === undefined ||
|
|
732
|
+
snapshot.cursor > tail
|
|
733
|
+
)
|
|
734
|
+
return false
|
|
735
|
+
if (observed.snapshot.cursor > snapshot.cursor && tail >= observed.snapshot.cursor)
|
|
736
|
+
return false
|
|
737
|
+
this.snapshots.set(cacheKey, JSON.parse(JSON.stringify(snapshot)))
|
|
738
|
+
return true
|
|
739
|
+
},
|
|
740
|
+
})
|
|
741
|
+
}
|
|
742
|
+
|
|
717
743
|
async getReducerSnapshot(
|
|
718
744
|
scope: SnapshotScope,
|
|
719
745
|
reducer: string,
|
|
720
746
|
version: number,
|
|
721
747
|
): Promise<ReducerSnapshot | null> {
|
|
748
|
+
assertPublicSnapshotReducer(reducer)
|
|
722
749
|
const tail = this.snapshotTail(scope)
|
|
723
750
|
if (tail === undefined) return null
|
|
724
751
|
const stored = this.snapshots.get(`${snapshotScopeKey(scope)}\u0000${reducer}`)
|
|
@@ -730,6 +757,15 @@ export class MemoryBuildStore implements BuildStore {
|
|
|
730
757
|
scope: SnapshotScope,
|
|
731
758
|
reducer: string,
|
|
732
759
|
snapshot: ReducerSnapshot,
|
|
760
|
+
): Promise<boolean> {
|
|
761
|
+
assertPublicSnapshotReducer(reducer)
|
|
762
|
+
return this.putSnapshot(scope, reducer, snapshot)
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private async putSnapshot(
|
|
766
|
+
scope: SnapshotScope,
|
|
767
|
+
reducer: string,
|
|
768
|
+
snapshot: ReducerSnapshot,
|
|
733
769
|
): Promise<boolean> {
|
|
734
770
|
const tail = this.snapshotTail(scope)
|
|
735
771
|
if (tail === undefined || snapshot.cursor > tail) return false
|
|
@@ -896,6 +932,60 @@ export class MemoryBuildStore implements BuildStore {
|
|
|
896
932
|
return structuredClone(latest.meta)
|
|
897
933
|
}
|
|
898
934
|
|
|
935
|
+
async beginRepoTick(repo: string, holder: string): Promise<string | null> {
|
|
936
|
+
const state = this.repoState(repo)
|
|
937
|
+
if (
|
|
938
|
+
!state.lease ||
|
|
939
|
+
state.lease.holder !== holder ||
|
|
940
|
+
state.lease.expiresAt <= this.clock().getTime()
|
|
941
|
+
)
|
|
942
|
+
return null
|
|
943
|
+
const startedAt = this.now()
|
|
944
|
+
state.tick = {
|
|
945
|
+
run: holder,
|
|
946
|
+
open: { startedAt },
|
|
947
|
+
...(state.tick?.finished ? { finished: state.tick.finished } : {}),
|
|
948
|
+
}
|
|
949
|
+
state.record.updatedAt = startedAt
|
|
950
|
+
return startedAt
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
async finishRepoTick(
|
|
954
|
+
repo: string,
|
|
955
|
+
holder: string,
|
|
956
|
+
completion: RepoTickCompletion,
|
|
957
|
+
events: RepositoryEventWrite[],
|
|
958
|
+
): Promise<RepoTickFinish | null> {
|
|
959
|
+
const validated = validateTickFinish(holder, completion, events)
|
|
960
|
+
const state = this.repoState(repo)
|
|
961
|
+
if (
|
|
962
|
+
!state.lease ||
|
|
963
|
+
state.lease.holder !== holder ||
|
|
964
|
+
state.lease.expiresAt <= this.clock().getTime()
|
|
965
|
+
)
|
|
966
|
+
return null
|
|
967
|
+
if (state.tick?.run !== holder || !state.tick.open) throw new Error('no open repository tick')
|
|
968
|
+
const completedAt = this.now()
|
|
969
|
+
const envelopes = validated.map((event, index) => ({
|
|
970
|
+
...event,
|
|
971
|
+
repo,
|
|
972
|
+
seq: state.events.length + index + 1,
|
|
973
|
+
ts: completedAt,
|
|
974
|
+
})) as RepositoryEvent[]
|
|
975
|
+
state.tick = {
|
|
976
|
+
run: holder,
|
|
977
|
+
finished: {
|
|
978
|
+
...structuredClone(completion),
|
|
979
|
+
run: holder,
|
|
980
|
+
startedAt: state.tick.open.startedAt,
|
|
981
|
+
completedAt,
|
|
982
|
+
},
|
|
983
|
+
}
|
|
984
|
+
state.events.push(...structuredClone(envelopes))
|
|
985
|
+
state.record.updatedAt = completedAt
|
|
986
|
+
return structuredClone({ observation: state.tick, events: envelopes })
|
|
987
|
+
}
|
|
988
|
+
|
|
899
989
|
async claimRepoLease(repo: string, holder: string, ttlMs: number): Promise<boolean> {
|
|
900
990
|
const state = this.repoState(repo)
|
|
901
991
|
const now = this.clock().getTime()
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReducerValueRequest, ReducerValue } from './reducer-values'
|
|
1
2
|
/**
|
|
2
3
|
* Local ambient phase-session wrapper for a BuildStore.
|
|
3
4
|
*
|
|
@@ -333,6 +334,14 @@ export function scopeLocalStoreToPhaseSession(
|
|
|
333
334
|
own('getRepoHighWater', 'repo', repo)
|
|
334
335
|
return store.getRepoHighWater(repo)
|
|
335
336
|
},
|
|
337
|
+
async getReducerValue(
|
|
338
|
+
target: SnapshotScope,
|
|
339
|
+
request: ReducerValueRequest,
|
|
340
|
+
): Promise<ReducerValue> {
|
|
341
|
+
if (target.kind === 'build') own('getReducerValue', 'build', target.slug)
|
|
342
|
+
else own('getReducerValue', 'repo', target.repo)
|
|
343
|
+
return store.getReducerValue(target, request)
|
|
344
|
+
},
|
|
336
345
|
async getReducerSnapshot(
|
|
337
346
|
target: SnapshotScope,
|
|
338
347
|
reducer: string,
|
|
@@ -423,6 +432,14 @@ export function scopeLocalStoreToPhaseSession(
|
|
|
423
432
|
own('claimRepoLease', 'repo', repo)
|
|
424
433
|
return store.claimRepoLease(repo, holder, ttlMs)
|
|
425
434
|
},
|
|
435
|
+
async beginRepoTick(repo: string) {
|
|
436
|
+
own('beginRepoTick', 'repo', repo)
|
|
437
|
+
throw new Error('phase sessions cannot begin repository ticks')
|
|
438
|
+
},
|
|
439
|
+
async finishRepoTick(repo: string) {
|
|
440
|
+
own('finishRepoTick', 'repo', repo)
|
|
441
|
+
throw new Error('phase sessions cannot finish repository ticks')
|
|
442
|
+
},
|
|
426
443
|
async heartbeatRepo(repo: string, holder: string): Promise<boolean> {
|
|
427
444
|
own('heartbeatRepo', 'repo', repo)
|
|
428
445
|
return store.heartbeatRepo(repo, holder)
|