@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
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { JsonValue } from './reducer-values'
|
|
2
|
+
import shapes from './reducer-value-shapes.json'
|
|
3
|
+
|
|
4
|
+
type Shape = {
|
|
5
|
+
kind: string
|
|
6
|
+
value?: unknown
|
|
7
|
+
items?: string | string[]
|
|
8
|
+
props?: Record<string, { type: string; optional: boolean }>
|
|
9
|
+
index?: string
|
|
10
|
+
}
|
|
11
|
+
const schemas: Record<string, Shape> = shapes.schemas
|
|
12
|
+
|
|
13
|
+
/** Generated structural shapes include nested event payload unions, records,
|
|
14
|
+
* optional fields and literal discriminants. No accumulator is trusted merely
|
|
15
|
+
* because finish() happens to accept it. */
|
|
16
|
+
export function validateAccumulator(name: string, value: unknown): boolean {
|
|
17
|
+
const root = (shapes.roots as Record<string, { acc: string }>)[name]
|
|
18
|
+
return root !== undefined && matches(root.acc, value)
|
|
19
|
+
}
|
|
20
|
+
export function validateReducerValue(name: string, value: unknown): boolean {
|
|
21
|
+
const root = (shapes.roots as Record<string, { value: string }>)[name]
|
|
22
|
+
return root !== undefined && matches(root.value, value)
|
|
23
|
+
}
|
|
24
|
+
function json(value: unknown): boolean {
|
|
25
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return true
|
|
26
|
+
if (typeof value === 'number') return Number.isFinite(value)
|
|
27
|
+
if (Array.isArray(value)) return value.every(json)
|
|
28
|
+
return (
|
|
29
|
+
typeof value === 'object' &&
|
|
30
|
+
Object.getPrototypeOf(value) === Object.prototype &&
|
|
31
|
+
Object.values(value).every(json)
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
function matches(id: string, value: unknown): boolean {
|
|
35
|
+
const shape = schemas[id]!
|
|
36
|
+
switch (shape.kind) {
|
|
37
|
+
case 'json':
|
|
38
|
+
return json(value)
|
|
39
|
+
case 'undefined':
|
|
40
|
+
return value === undefined
|
|
41
|
+
case 'literal':
|
|
42
|
+
return value === shape.value
|
|
43
|
+
case 'number':
|
|
44
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
45
|
+
case 'string':
|
|
46
|
+
return typeof value === 'string'
|
|
47
|
+
case 'boolean':
|
|
48
|
+
return typeof value === 'boolean'
|
|
49
|
+
case 'union':
|
|
50
|
+
return (shape.items as string[]).some((item) => matches(item, value))
|
|
51
|
+
case 'array':
|
|
52
|
+
return Array.isArray(value) && value.every((item) => matches(shape.items as string, item))
|
|
53
|
+
case 'object': {
|
|
54
|
+
if (
|
|
55
|
+
value === null ||
|
|
56
|
+
typeof value !== 'object' ||
|
|
57
|
+
Array.isArray(value) ||
|
|
58
|
+
Object.getPrototypeOf(value) !== Object.prototype
|
|
59
|
+
)
|
|
60
|
+
return false
|
|
61
|
+
const record = value as Record<string, unknown>
|
|
62
|
+
for (const [key, prop] of Object.entries(shape.props!)) {
|
|
63
|
+
if (!(key in record) && prop.optional) continue
|
|
64
|
+
if (!matches(prop.type, record[key])) return false
|
|
65
|
+
}
|
|
66
|
+
return Object.keys(record).every(
|
|
67
|
+
(key) =>
|
|
68
|
+
Object.hasOwn(shape.props!, key) ||
|
|
69
|
+
(shape.index !== undefined && matches(shape.index, record[key])),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
default:
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Existing optional object fields keep their ordinary JSON omission semantics.
|
|
78
|
+
* Collections, functions, nonfinite numbers and undefined array entries cannot
|
|
79
|
+
* silently become a different public contract during JSON serialization. */
|
|
80
|
+
export function encodeReducerValue(value: unknown): JsonValue {
|
|
81
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
82
|
+
return value as JsonValue
|
|
83
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
84
|
+
if (Array.isArray(value)) return value.map(encodeReducerValue)
|
|
85
|
+
if (
|
|
86
|
+
value !== null &&
|
|
87
|
+
typeof value === 'object' &&
|
|
88
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
89
|
+
)
|
|
90
|
+
return Object.fromEntries(
|
|
91
|
+
Object.entries(value)
|
|
92
|
+
.filter(([, item]) => item !== undefined)
|
|
93
|
+
.map(([key, item]) => [key, encodeReducerValue(item)]),
|
|
94
|
+
)
|
|
95
|
+
throw new Error('reducer finished value is not plain JSON')
|
|
96
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/** Named JSON answers over persisted incremental state. The event log remains authoritative. */
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import type { AbEvent } from '../events/catalog'
|
|
4
|
+
import type { RepositoryEvent } from '../events/repository'
|
|
5
|
+
import { REDUCERS } from '../kernel/reducer-registry'
|
|
6
|
+
import type { IncrementalReducer } from '../kernel/incremental'
|
|
7
|
+
import {
|
|
8
|
+
REDUCER_VALUE_SNAPSHOT_PREFIX,
|
|
9
|
+
type ReducerSnapshot,
|
|
10
|
+
type SnapshotScope,
|
|
11
|
+
} from './snapshots'
|
|
12
|
+
|
|
13
|
+
export const reducerValueRequestSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
reducer: z.string().min(1).max(64),
|
|
16
|
+
args: z.record(z.string(), z.json()).optional(),
|
|
17
|
+
})
|
|
18
|
+
.strict()
|
|
19
|
+
export type ReducerValueRequest = z.infer<typeof reducerValueRequestSchema>
|
|
20
|
+
export type JsonValue = z.infer<ReturnType<typeof z.json>>
|
|
21
|
+
export interface ReducerValue {
|
|
22
|
+
version: number
|
|
23
|
+
cursor: number
|
|
24
|
+
value: JsonValue
|
|
25
|
+
}
|
|
26
|
+
export const reducerValueSchema = z
|
|
27
|
+
.object({
|
|
28
|
+
version: z.number().int().positive(),
|
|
29
|
+
cursor: z.number().int().nonnegative(),
|
|
30
|
+
value: z.json(),
|
|
31
|
+
})
|
|
32
|
+
.strict()
|
|
33
|
+
|
|
34
|
+
export interface ValueCatalogEntry {
|
|
35
|
+
scope: SnapshotScope['kind']
|
|
36
|
+
args: z.ZodType
|
|
37
|
+
validate(state: unknown): boolean
|
|
38
|
+
adapt(events: readonly (AbEvent | RepositoryEvent)[]): readonly unknown[]
|
|
39
|
+
value(state: unknown): JsonValue
|
|
40
|
+
// biome-ignore lint/suspicious/noExplicitAny: heterogeneous executable catalog
|
|
41
|
+
create(args: any): IncrementalReducer<any, any, any>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function canonical(value: unknown): string {
|
|
45
|
+
if (Array.isArray(value)) return `[${value.map(canonical).join(',')}]`
|
|
46
|
+
if (value !== null && typeof value === 'object')
|
|
47
|
+
return `{${Object.entries(value)
|
|
48
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
49
|
+
.map(([k, v]) => `${JSON.stringify(k)}:${canonical(v)}`)
|
|
50
|
+
.join(',')}}`
|
|
51
|
+
return JSON.stringify(value)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolveReducerValue(scope: SnapshotScope, input: ReducerValueRequest) {
|
|
55
|
+
const request = reducerValueRequestSchema.parse(input)
|
|
56
|
+
const registration = Object.hasOwn(REDUCERS, request.reducer)
|
|
57
|
+
? REDUCERS[request.reducer]
|
|
58
|
+
: undefined
|
|
59
|
+
if (!registration) throw new Error(`unknown reducer "${request.reducer}"`)
|
|
60
|
+
const entry = registration.valueRead
|
|
61
|
+
if ('excluded' in entry)
|
|
62
|
+
throw new Error(`excluded reducer "${request.reducer}": ${entry.excluded}`)
|
|
63
|
+
if (entry.scope !== scope.kind) throw new Error(`wrong scope for reducer "${request.reducer}"`)
|
|
64
|
+
const args = entry.args.parse(request.args ?? {})
|
|
65
|
+
const reducer = entry.create(args)
|
|
66
|
+
const identity = canonical({ scope, reducer: request.reducer, version: reducer.version, args })
|
|
67
|
+
// v2 ignores checkpoints from the former client-writable key format.
|
|
68
|
+
const key = `${REDUCER_VALUE_SNAPSHOT_PREFIX}v2.${new Bun.CryptoHasher('sha256').update(identity).digest('hex').slice(0, 54)}`
|
|
69
|
+
return { name: request.reducer, reducer, identity, key, entry }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Raw cache reads and conditional repairs are private adapter operations: clients
|
|
73
|
+
* cannot submit a new accumulator through the value API. `token` identifies the
|
|
74
|
+
* exact row read, including malformed bytes that could not be decoded. */
|
|
75
|
+
export interface ValueCacheRow {
|
|
76
|
+
snapshot: ReducerSnapshot
|
|
77
|
+
token: string
|
|
78
|
+
}
|
|
79
|
+
export interface ReducerValueStorage {
|
|
80
|
+
highWater(): Promise<number>
|
|
81
|
+
read(key: string): Promise<ValueCacheRow | null>
|
|
82
|
+
events(since: number, through: number): Promise<(AbEvent | RepositoryEvent)[]>
|
|
83
|
+
put(key: string, snapshot: ReducerSnapshot): Promise<boolean>
|
|
84
|
+
repair(key: string, observed: ValueCacheRow, snapshot: ReducerSnapshot): Promise<boolean>
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function readReducerValue(
|
|
88
|
+
scope: SnapshotScope,
|
|
89
|
+
request: ReducerValueRequest,
|
|
90
|
+
storage: ReducerValueStorage,
|
|
91
|
+
): Promise<ReducerValue> {
|
|
92
|
+
const { reducer, identity, key, entry } = resolveReducerValue(scope, request)
|
|
93
|
+
let boundary = await storage.highWater()
|
|
94
|
+
let row: ValueCacheRow | null = null
|
|
95
|
+
try {
|
|
96
|
+
row = await storage.read(key)
|
|
97
|
+
} catch {
|
|
98
|
+
/* unreadable cache is a miss */
|
|
99
|
+
}
|
|
100
|
+
// A concurrent reader may have persisted beyond our first boundary. Only a
|
|
101
|
+
// fresh authoritative boundary can establish ahead-of-log poison.
|
|
102
|
+
if (row && row.snapshot.cursor > boundary) boundary = await storage.highWater()
|
|
103
|
+
let acc = reducer.initial()
|
|
104
|
+
let cursor = 0
|
|
105
|
+
let valid = false
|
|
106
|
+
if (row) {
|
|
107
|
+
const { snapshot } = row
|
|
108
|
+
const state = snapshot.state as { identity?: unknown; acc?: unknown } | null
|
|
109
|
+
try {
|
|
110
|
+
valid =
|
|
111
|
+
snapshot.version === reducer.version &&
|
|
112
|
+
Number.isSafeInteger(snapshot.cursor) &&
|
|
113
|
+
snapshot.cursor >= 0 &&
|
|
114
|
+
snapshot.cursor <= boundary &&
|
|
115
|
+
state !== null &&
|
|
116
|
+
typeof state === 'object' &&
|
|
117
|
+
state.identity === identity &&
|
|
118
|
+
entry.validate(state.acc)
|
|
119
|
+
} catch {
|
|
120
|
+
valid = false // malformed nested cache data must never prevent replay
|
|
121
|
+
}
|
|
122
|
+
if (valid) {
|
|
123
|
+
acc = structuredClone(state!.acc)
|
|
124
|
+
cursor = snapshot.cursor
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const delta = (await storage.events(cursor, boundary)).filter((event) => event.seq <= boundary)
|
|
128
|
+
for (const event of delta) {
|
|
129
|
+
if (event.seq !== cursor + 1) throw new Error('noncontiguous reducer log')
|
|
130
|
+
cursor = event.seq
|
|
131
|
+
}
|
|
132
|
+
if (cursor !== boundary) throw new Error('incomplete reducer log')
|
|
133
|
+
acc = reducer.advance(acc, entry.adapt(delta))
|
|
134
|
+
const value = entry.value(reducer.finish(acc))
|
|
135
|
+
if (!valid || delta.length > 0) {
|
|
136
|
+
const next = { version: reducer.version, cursor, state: { identity, acc } }
|
|
137
|
+
try {
|
|
138
|
+
if (row && !valid) await storage.repair(key, row, next)
|
|
139
|
+
else await storage.put(key, next)
|
|
140
|
+
} catch {
|
|
141
|
+
/* best-effort cache; log and reduction errors propagate */
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { version: reducer.version, cursor, value }
|
|
145
|
+
}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveReducerValue,
|
|
3
|
+
reducerValueSchema,
|
|
4
|
+
type ReducerValueRequest,
|
|
5
|
+
type ReducerValue,
|
|
6
|
+
} from '../reducer-values'
|
|
1
7
|
/**
|
|
2
8
|
* RemoteBuildStore (SPEC §7.2 adapter 2): the client half of the remote
|
|
3
9
|
* store — a full BuildStore over the wire protocol in protocol.ts. The
|
|
@@ -13,6 +19,8 @@
|
|
|
13
19
|
* `subscribe` is the shared polling implementation over `getEvents(since)`
|
|
14
20
|
* (§7.2); `close()` is a no-op — the server owns the backing store.
|
|
15
21
|
*/
|
|
22
|
+
import { beginRepoTickResponseSchema, finishRepoTickResponseSchema } from './protocol'
|
|
23
|
+
import type { RepoTickCompletion, RepoTickFinish } from '../types'
|
|
16
24
|
import {
|
|
17
25
|
EventValidationError,
|
|
18
26
|
type AbEvent,
|
|
@@ -539,6 +547,17 @@ export class RemoteBuildStore implements BuildStore {
|
|
|
539
547
|
)
|
|
540
548
|
}
|
|
541
549
|
|
|
550
|
+
async getReducerValue(scope: SnapshotScope, request: ReducerValueRequest): Promise<ReducerValue> {
|
|
551
|
+
resolveReducerValue(scope, request)
|
|
552
|
+
const root = scope.kind === 'build' ? this.buildPath(scope.slug) : this.repoPath(scope.repo)
|
|
553
|
+
return this.requestJson(
|
|
554
|
+
'POST',
|
|
555
|
+
`${root}/reducer-values`,
|
|
556
|
+
reducerValueSchema,
|
|
557
|
+
request,
|
|
558
|
+
) as Promise<ReducerValue>
|
|
559
|
+
}
|
|
560
|
+
|
|
542
561
|
private snapshotPath(scope: SnapshotScope, reducer: string): string {
|
|
543
562
|
const root = scope.kind === 'build' ? this.buildPath(scope.slug) : this.repoPath(scope.repo)
|
|
544
563
|
return `${root}/snapshots/${encodeURIComponent(reducer)}`
|
|
@@ -822,6 +841,28 @@ export class RemoteBuildStore implements BuildStore {
|
|
|
822
841
|
)
|
|
823
842
|
}
|
|
824
843
|
|
|
844
|
+
async beginRepoTick(repo: string, holder: string): Promise<string | null> {
|
|
845
|
+
return this.requestJson(
|
|
846
|
+
'POST',
|
|
847
|
+
`${this.repoPath(repo)}/tick/begin`,
|
|
848
|
+
beginRepoTickResponseSchema,
|
|
849
|
+
{ holder },
|
|
850
|
+
)
|
|
851
|
+
}
|
|
852
|
+
async finishRepoTick(
|
|
853
|
+
repo: string,
|
|
854
|
+
holder: string,
|
|
855
|
+
completion: RepoTickCompletion,
|
|
856
|
+
events: RepositoryEventWrite[],
|
|
857
|
+
): Promise<RepoTickFinish | null> {
|
|
858
|
+
return (await this.requestJson(
|
|
859
|
+
'POST',
|
|
860
|
+
`${this.repoPath(repo)}/tick/finish`,
|
|
861
|
+
finishRepoTickResponseSchema,
|
|
862
|
+
{ holder, completion, events },
|
|
863
|
+
)) as RepoTickFinish | null
|
|
864
|
+
}
|
|
865
|
+
|
|
825
866
|
async claimRepoLease(repo: string, holder: string, ttlMs: number): Promise<boolean> {
|
|
826
867
|
const result = await this.requestJson(
|
|
827
868
|
'POST',
|
|
@@ -68,6 +68,8 @@ export const RELEASE_LINE_PROTOCOLS: Readonly<Record<string, string>> = {
|
|
|
68
68
|
'0.10': '3',
|
|
69
69
|
'0.11': '3',
|
|
70
70
|
'0.12': '3',
|
|
71
|
+
// v0.13.0 shipped protocol 3. Protocol 4 requires the next minor line.
|
|
72
|
+
'0.13': '3',
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
export function clientIdentityError(
|
|
@@ -35,3 +35,22 @@ export { delegateCredential } from './credentials'
|
|
|
35
35
|
|
|
36
36
|
export { reduceBuild } from '../../kernel/reducer'
|
|
37
37
|
export { reduceHarvest } from '../../kernel/harvest'
|
|
38
|
+
|
|
39
|
+
export {
|
|
40
|
+
readReducerValue,
|
|
41
|
+
resolveReducerValue,
|
|
42
|
+
reducerValueRequestSchema,
|
|
43
|
+
reducerValueSchema,
|
|
44
|
+
} from '../reducer-values'
|
|
45
|
+
export type {
|
|
46
|
+
ReducerValue,
|
|
47
|
+
ReducerValueRequest,
|
|
48
|
+
ReducerValueStorage,
|
|
49
|
+
ValueCacheRow,
|
|
50
|
+
} from '../reducer-values'
|
|
51
|
+
|
|
52
|
+
export type { BuildDelegationFacts, HarvestDelegationFacts } from '../../kernel/service-facts'
|
|
53
|
+
|
|
54
|
+
export { delegationFactKey } from '../../kernel/service-facts'
|
|
55
|
+
|
|
56
|
+
export { assertPublicSnapshotReducer } from '../snapshots'
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* Arbitrary computation over the assigned revisions inside `makeEvent`
|
|
25
25
|
* (e.g. `rev + 1`, string interpolation) is unsupported by design.
|
|
26
26
|
*/
|
|
27
|
+
import { repoTickCompletionSchema, repoTickObservationSchema } from '../repo-tick'
|
|
27
28
|
import { reducerSnapshotWireSchema } from '../snapshots'
|
|
28
29
|
import { z } from 'zod'
|
|
29
30
|
import { actorSchema } from '../../events/envelope'
|
|
@@ -104,6 +105,7 @@ export const conditionalEventResponseSchema = eventEnvelopeWireSchema.nullable()
|
|
|
104
105
|
|
|
105
106
|
export const ensureRepoBodySchema = z.object({ repo: z.string().min(1) })
|
|
106
107
|
export const repositoryRecordWireSchema = z.object({
|
|
108
|
+
tick: repoTickObservationSchema.optional(),
|
|
107
109
|
repo: z.string().min(1),
|
|
108
110
|
createdAt: z.string(),
|
|
109
111
|
updatedAt: z.string(),
|
|
@@ -475,3 +477,17 @@ export const delegateCredentialBodySchema = z.discriminatedUnion('kind', [
|
|
|
475
477
|
])
|
|
476
478
|
export type DelegateCredentialTarget = z.infer<typeof delegateCredentialBodySchema>
|
|
477
479
|
export const delegateCredentialResponseSchema = z.strictObject({ token: z.string().min(1) })
|
|
480
|
+
|
|
481
|
+
export { reducerValueRequestSchema, reducerValueSchema } from '../reducer-values'
|
|
482
|
+
export const beginRepoTickResponseSchema = z.iso.datetime().nullable()
|
|
483
|
+
export const finishRepoTickBodySchema = z.object({
|
|
484
|
+
holder: z.string().min(1),
|
|
485
|
+
completion: repoTickCompletionSchema,
|
|
486
|
+
events: z.array(z.object({ actor: actorSchema, type: z.string(), payload: z.unknown() })),
|
|
487
|
+
})
|
|
488
|
+
export const finishRepoTickResponseSchema = z
|
|
489
|
+
.object({
|
|
490
|
+
observation: repoTickObservationSchema,
|
|
491
|
+
events: z.array(repositoryEventEnvelopeWireSchema),
|
|
492
|
+
})
|
|
493
|
+
.nullable()
|
|
@@ -4,11 +4,13 @@ import manifest from '../../../../../package.json'
|
|
|
4
4
|
export const AUTOBUILD_VERSION = manifest.version
|
|
5
5
|
|
|
6
6
|
/** Wire compatibility version for the remote BuildStore protocol.
|
|
7
|
+
* Version 4: adds required fenced begin/finish tick operations and bounded
|
|
8
|
+
* repository tick telemetry.
|
|
7
9
|
* Version 3 (AUT-521): the build-digest wire shape gains per-occurrence
|
|
8
10
|
* observation timestamps and the `merged` fact — a breaking shape change,
|
|
9
11
|
* so stale peers are rejected at handshake instead of failing mid-request
|
|
10
12
|
* at schema parse. */
|
|
11
|
-
export const REMOTE_STORE_PROTOCOL_VERSION = '
|
|
13
|
+
export const REMOTE_STORE_PROTOCOL_VERSION = '4'
|
|
12
14
|
|
|
13
15
|
export const AUTOBUILD_VERSION_HEADER = 'x-autobuild-version'
|
|
14
16
|
export const REMOTE_STORE_PROTOCOL_VERSION_HEADER = 'x-autobuild-protocol-version'
|
|
@@ -142,7 +142,7 @@ export const repositoryStateEventsReducer: IncrementalReducer<
|
|
|
142
142
|
* `RepositoryStateEventsAcc` it prunes on every `dispatcher.run-started`
|
|
143
143
|
* (everything before the new anchor that is not durable leaves the tail), which
|
|
144
144
|
* is exact for seq-ordered input — the only input a real journal produces — and
|
|
145
|
-
*
|
|
145
|
+
* holds the tail only in process memory. It is never persisted as a snapshot. */
|
|
146
146
|
export interface JournalViewAcc {
|
|
147
147
|
anchor?: number
|
|
148
148
|
retained: RepositoryEvent[]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { EventValidationError } from '../events/catalog'
|
|
3
|
+
import {
|
|
4
|
+
dispatcherStatusEventPayloadSchemas,
|
|
5
|
+
validateRepositoryEventWrite,
|
|
6
|
+
type RepositoryEventWrite,
|
|
7
|
+
} from '../events/repository'
|
|
8
|
+
import type { RepoTickCompletion } from './types'
|
|
9
|
+
|
|
10
|
+
export const repoTickCompletionSchema = z.discriminatedUnion('outcome', [
|
|
11
|
+
z.object({
|
|
12
|
+
outcome: z.literal('completed'),
|
|
13
|
+
counters: dispatcherStatusEventPayloadSchemas['dispatcher.tick-completed'].shape.counters,
|
|
14
|
+
}),
|
|
15
|
+
z.object({ outcome: z.literal('failed'), error: z.string().min(1) }),
|
|
16
|
+
])
|
|
17
|
+
export const repoTickObservationSchema = z.object({
|
|
18
|
+
run: z.string().min(1),
|
|
19
|
+
open: z.object({ startedAt: z.iso.datetime() }).optional(),
|
|
20
|
+
finished: z
|
|
21
|
+
.intersection(
|
|
22
|
+
repoTickCompletionSchema,
|
|
23
|
+
z.object({
|
|
24
|
+
run: z.string().min(1),
|
|
25
|
+
startedAt: z.iso.datetime(),
|
|
26
|
+
completedAt: z.iso.datetime(),
|
|
27
|
+
}),
|
|
28
|
+
)
|
|
29
|
+
.optional(),
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
/** Only a matching start and terminal boundary may ride the atomic finish. */
|
|
33
|
+
export function validateTickFinish(
|
|
34
|
+
holder: string,
|
|
35
|
+
completion: RepoTickCompletion,
|
|
36
|
+
events: RepositoryEventWrite[],
|
|
37
|
+
) {
|
|
38
|
+
const parsed = repoTickCompletionSchema.parse(completion)
|
|
39
|
+
const validated = events.map(validateRepositoryEventWrite)
|
|
40
|
+
if (validated.length === 0 && completion.outcome === 'completed') return validated
|
|
41
|
+
if (validated.length !== 2)
|
|
42
|
+
throw new EventValidationError('tick finish requires start and terminal boundaries')
|
|
43
|
+
const [start, end] = validated
|
|
44
|
+
if (
|
|
45
|
+
start!.type !== 'dispatcher.tick-started' ||
|
|
46
|
+
end!.type !==
|
|
47
|
+
(completion.outcome === 'completed'
|
|
48
|
+
? 'dispatcher.tick-completed'
|
|
49
|
+
: 'dispatcher.tick-failed') ||
|
|
50
|
+
validated.some(
|
|
51
|
+
(e) => e.actor.kind !== 'dispatcher' || !('run' in e.payload) || e.payload.run !== holder,
|
|
52
|
+
)
|
|
53
|
+
) {
|
|
54
|
+
throw new EventValidationError('invalid tick finish boundaries')
|
|
55
|
+
}
|
|
56
|
+
if (
|
|
57
|
+
end!.type === 'dispatcher.tick-failed' &&
|
|
58
|
+
completion.outcome === 'failed' &&
|
|
59
|
+
dispatcherStatusEventPayloadSchemas['dispatcher.tick-failed'].parse(end!.payload).error !==
|
|
60
|
+
completion.error
|
|
61
|
+
)
|
|
62
|
+
throw new EventValidationError('tick error mismatch')
|
|
63
|
+
if (
|
|
64
|
+
end!.type === 'dispatcher.tick-completed' &&
|
|
65
|
+
completion.outcome === 'completed' &&
|
|
66
|
+
JSON.stringify(
|
|
67
|
+
dispatcherStatusEventPayloadSchemas['dispatcher.tick-completed'].parse(end!.payload).counters,
|
|
68
|
+
) !== JSON.stringify(parsed.outcome === 'completed' ? parsed.counters : {})
|
|
69
|
+
)
|
|
70
|
+
throw new EventValidationError('tick counters mismatch')
|
|
71
|
+
return validated
|
|
72
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReducerValueRequest, ReducerValue } from './reducer-values'
|
|
1
2
|
import type { ReducerSnapshot, SnapshotScope } from './snapshots'
|
|
2
3
|
import type { EventType } from '../events/payloads'
|
|
3
4
|
import type { EventWrite } from '../events/catalog'
|
|
@@ -175,6 +176,18 @@ export function createSessionScopedStore(store: BuildStore, scope: string): Sess
|
|
|
175
176
|
getRepoHighWater(repo: string): Promise<number> {
|
|
176
177
|
return Promise.reject(new SessionScopeError(scope, 'getRepoHighWater', repo))
|
|
177
178
|
},
|
|
179
|
+
async getReducerValue(
|
|
180
|
+
target: SnapshotScope,
|
|
181
|
+
_request: ReducerValueRequest,
|
|
182
|
+
): Promise<ReducerValue> {
|
|
183
|
+
return Promise.reject(
|
|
184
|
+
new SessionScopeError(
|
|
185
|
+
scope,
|
|
186
|
+
'getReducerValue',
|
|
187
|
+
target.kind === 'build' ? target.slug : target.repo,
|
|
188
|
+
),
|
|
189
|
+
)
|
|
190
|
+
},
|
|
178
191
|
getReducerSnapshot(
|
|
179
192
|
target: SnapshotScope,
|
|
180
193
|
_reducer: string,
|
|
@@ -262,6 +275,12 @@ export function createSessionScopedStore(store: BuildStore, scope: string): Sess
|
|
|
262
275
|
claimRepoLease(repo: string, _holder: string, _ttlMs: number): Promise<boolean> {
|
|
263
276
|
return Promise.reject(new SessionScopeError(scope, 'claimRepoLease', repo))
|
|
264
277
|
},
|
|
278
|
+
beginRepoTick(repo: string) {
|
|
279
|
+
return Promise.reject(new SessionScopeError(scope, 'beginRepoTick', repo))
|
|
280
|
+
},
|
|
281
|
+
finishRepoTick(repo: string) {
|
|
282
|
+
return Promise.reject(new SessionScopeError(scope, 'finishRepoTick', repo))
|
|
283
|
+
},
|
|
265
284
|
heartbeatRepo(repo: string, _holder: string): Promise<boolean> {
|
|
266
285
|
return Promise.reject(new SessionScopeError(scope, 'heartbeatRepo', repo))
|
|
267
286
|
},
|
|
@@ -36,6 +36,16 @@ export const reducerSnapshotWireSchema = z.object({
|
|
|
36
36
|
* path segment and a SQL key. */
|
|
37
37
|
export const SNAPSHOT_REDUCER_NAME = /^[A-Za-z][A-Za-z0-9._-]{0,63}$/
|
|
38
38
|
|
|
39
|
+
/** Value checkpoints are written only by the adapter's private reduction path.
|
|
40
|
+
* Generic snapshot clients must never supply state used for named answers. */
|
|
41
|
+
export const REDUCER_VALUE_SNAPSHOT_PREFIX = 'value.'
|
|
42
|
+
|
|
43
|
+
export function assertPublicSnapshotReducer(reducer: string): void {
|
|
44
|
+
if (reducer.startsWith(REDUCER_VALUE_SNAPSHOT_PREFIX)) {
|
|
45
|
+
throw new Error('reserved reducer value snapshot namespace')
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
export function snapshotScopeKey(scope: SnapshotScope): string {
|
|
40
50
|
return scope.kind === 'build' ? `build:${scope.slug}` : `repo:${scope.repo}`
|
|
41
51
|
}
|