@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.
Files changed (151) hide show
  1. package/README.md +1 -1
  2. package/SPEC.md +138 -16
  3. package/bin/ab-dispatch-kernel.ts +4 -6
  4. package/docs/architecture.md +155 -73
  5. package/docs/configuration.md +41 -9
  6. package/docs/hosted-service.md +31 -5
  7. package/docs/operator-api.md +8 -1
  8. package/docs/remote-store-protocol.md +94 -2
  9. package/docs/remote-ticket-protocol.md +151 -9
  10. package/docs/setup.md +33 -7
  11. package/docs/ticket-migration.md +64 -0
  12. package/package.json +4 -3
  13. package/packages/core/src/cli/attention.ts +18 -164
  14. package/packages/core/src/cli/build-control.ts +23 -16
  15. package/packages/core/src/cli/bulk-control.ts +3 -2
  16. package/packages/core/src/cli/connect.ts +141 -38
  17. package/packages/core/src/cli/context.ts +23 -79
  18. package/packages/core/src/cli/dashboard/model.ts +36 -22
  19. package/packages/core/src/cli/dashboard/poll.ts +1 -1
  20. package/packages/core/src/cli/dispatch-frontend.ts +24 -67
  21. package/packages/core/src/cli/dispatch-process.ts +14 -7
  22. package/packages/core/src/cli/dispatch-repository-feed.ts +196 -0
  23. package/packages/core/src/cli/dispatch.ts +304 -82
  24. package/packages/core/src/cli/dispatcher-liveness.ts +26 -19
  25. package/packages/core/src/cli/harvest.ts +84 -42
  26. package/packages/core/src/cli/help.ts +14 -8
  27. package/packages/core/src/cli/historical-build-state.ts +27 -0
  28. package/packages/core/src/cli/installation.ts +67 -13
  29. package/packages/core/src/cli/main.ts +23 -5
  30. package/packages/core/src/cli/mcp.ts +2 -1
  31. package/packages/core/src/cli/pr-attachments.ts +10 -13
  32. package/packages/core/src/cli/pr-summary-publication.ts +27 -60
  33. package/packages/core/src/cli/pr-summary.ts +43 -13
  34. package/packages/core/src/cli/publication-settlement.ts +10 -20
  35. package/packages/core/src/cli/repository-status.ts +38 -2
  36. package/packages/core/src/cli/self-update.ts +5 -1
  37. package/packages/core/src/cli/status.ts +62 -32
  38. package/packages/core/src/cli/terminals.ts +25 -142
  39. package/packages/core/src/cli/ticket-assets.ts +7 -2
  40. package/packages/core/src/cli/ticket-editor.ts +82 -0
  41. package/packages/core/src/cli/ticket.ts +166 -36
  42. package/packages/core/src/cli/wait.ts +91 -84
  43. package/packages/core/src/cli/watch.ts +60 -97
  44. package/packages/core/src/config/pipeline-source.ts +4 -3
  45. package/packages/core/src/config/schema.ts +35 -3
  46. package/packages/core/src/connection.ts +3 -3
  47. package/packages/core/src/events/catalog.ts +3 -2
  48. package/packages/core/src/events/payloads.ts +6 -0
  49. package/packages/core/src/events/repository.ts +7 -3
  50. package/packages/core/src/integration/harness.ts +38 -4
  51. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +2444 -9
  52. package/packages/core/src/kernel/auto-merge.ts +19 -3
  53. package/packages/core/src/kernel/dispatcher-liveness.ts +10 -4
  54. package/packages/core/src/kernel/generators/build-log.ts +1 -1
  55. package/packages/core/src/kernel/generators/repository-journals.ts +14 -3
  56. package/packages/core/src/kernel/harvest.ts +25 -23
  57. package/packages/core/src/kernel/incremental-fixtures.ts +13 -2
  58. package/packages/core/src/kernel/log-index-value.ts +38 -0
  59. package/packages/core/src/kernel/read-facts.ts +637 -0
  60. package/packages/core/src/kernel/reducer-registry.ts +139 -3
  61. package/packages/core/src/kernel/repository-attention.ts +179 -0
  62. package/packages/core/src/kernel/service-facts.ts +135 -0
  63. package/packages/core/src/kernel/terminal-guard.ts +133 -0
  64. package/packages/core/src/ontology.ts +1 -0
  65. package/packages/core/src/operator/annotations.ts +12 -0
  66. package/packages/core/src/operator/control.ts +5 -6
  67. package/packages/core/src/operator/index.ts +7 -0
  68. package/packages/core/src/operator/protocol.ts +19 -2
  69. package/packages/core/src/operator/query.ts +37 -20
  70. package/packages/core/src/operator/read-view.ts +4 -13
  71. package/packages/core/src/operator/registry.ts +61 -8
  72. package/packages/core/src/operator/requests.ts +2 -14
  73. package/packages/core/src/operator/sandbox.ts +10 -9
  74. package/packages/core/src/operator/ticket-source-backend.ts +17 -21
  75. package/packages/core/src/operator/tickets.ts +132 -22
  76. package/packages/core/src/plugin-sdk/index.ts +19 -0
  77. package/packages/core/src/plugins/runtimes.ts +5 -1
  78. package/packages/core/src/ports/runner/session-env.ts +2 -0
  79. package/packages/core/src/ports/tickets/contract.ts +66 -1
  80. package/packages/core/src/ports/tickets/create.ts +48 -12
  81. package/packages/core/src/ports/tickets/fake.ts +30 -1
  82. package/packages/core/src/ports/tickets/file-assets.ts +159 -0
  83. package/packages/core/src/ports/tickets/file-codec.ts +90 -0
  84. package/packages/core/src/ports/tickets/file.ts +114 -120
  85. package/packages/core/src/ports/tickets/grants.ts +47 -0
  86. package/packages/core/src/ports/tickets/hosted-index.ts +17 -0
  87. package/packages/core/src/ports/tickets/identity.ts +20 -0
  88. package/packages/core/src/ports/tickets/lifecycle.ts +18 -0
  89. package/packages/core/src/ports/tickets/linear.ts +74 -9
  90. package/packages/core/src/ports/tickets/migration.ts +336 -0
  91. package/packages/core/src/ports/tickets/operations.ts +38 -7
  92. package/packages/core/src/ports/tickets/portable-assets.ts +54 -0
  93. package/packages/core/src/ports/tickets/remote-protocol.ts +95 -0
  94. package/packages/core/src/ports/tickets/remote.ts +60 -3
  95. package/packages/core/src/ports/tickets/requested-id.ts +46 -0
  96. package/packages/core/src/ports/tickets/revision.ts +62 -0
  97. package/packages/core/src/ports/tickets/states.ts +61 -0
  98. package/packages/core/src/ports/types.ts +40 -3
  99. package/packages/core/src/ports/workspace/build-execution.ts +1 -0
  100. package/packages/core/src/ports/workspace/create.ts +5 -1
  101. package/packages/core/src/ports/workspace/operator-sandbox.ts +2 -0
  102. package/packages/core/src/ports/workspace/vercel-sandbox.ts +8 -4
  103. package/packages/core/src/processes/build-child.ts +6 -6
  104. package/packages/core/src/processes/build-runner.ts +62 -70
  105. package/packages/core/src/processes/dispatch-failure-budget.ts +5 -4
  106. package/packages/core/src/processes/dispatcher.ts +223 -205
  107. package/packages/core/src/processes/execution-no-progress.ts +176 -0
  108. package/packages/core/src/processes/harvest-child.ts +30 -4
  109. package/packages/core/src/processes/harvest-execution-state.ts +15 -9
  110. package/packages/core/src/processes/harvest-runner.ts +58 -74
  111. package/packages/core/src/processes/harvest.ts +72 -29
  112. package/packages/core/src/processes/infrastructure-failure-budget.ts +13 -15
  113. package/packages/core/src/processes/orchestrator-tick.ts +2 -2
  114. package/packages/core/src/processes/phase-failure-budget.ts +1 -0
  115. package/packages/core/src/processes/publication-loss.ts +35 -21
  116. package/packages/core/src/processes/repo-view.ts +229 -1099
  117. package/packages/core/src/processes/tick-publication.ts +110 -0
  118. package/packages/core/src/store/adapter.ts +23 -0
  119. package/packages/core/src/store/build-scope.ts +17 -0
  120. package/packages/core/src/store/contract.ts +283 -1
  121. package/packages/core/src/store/local/schema.ts +1 -0
  122. package/packages/core/src/store/local/store.ts +180 -55
  123. package/packages/core/src/store/memory.ts +119 -29
  124. package/packages/core/src/store/phase-session.ts +17 -0
  125. package/packages/core/src/store/reduced-reads.ts +239 -0
  126. package/packages/core/src/store/reducer-value-shapes.json +14056 -0
  127. package/packages/core/src/store/reducer-value-validation.ts +96 -0
  128. package/packages/core/src/store/reducer-values.ts +145 -0
  129. package/packages/core/src/store/remote/client.ts +41 -0
  130. package/packages/core/src/store/remote/compatibility.ts +2 -0
  131. package/packages/core/src/store/remote/index.ts +19 -0
  132. package/packages/core/src/store/remote/protocol.ts +16 -0
  133. package/packages/core/src/store/remote/version.ts +3 -1
  134. package/packages/core/src/store/repo-state-events.ts +1 -1
  135. package/packages/core/src/store/repo-tick.ts +72 -0
  136. package/packages/core/src/store/session-handle.ts +19 -0
  137. package/packages/core/src/store/snapshots.ts +10 -0
  138. package/packages/core/src/store/ticket-asset-snapshots.ts +192 -0
  139. package/packages/core/src/store/ticket-assets.ts +86 -8
  140. package/packages/core/src/store/types.ts +40 -3
  141. package/packages/core/src/testing/dispatch-read-budget.ts +215 -0
  142. package/packages/core/src/testing/index.ts +8 -0
  143. package/packages/core/src/testing/reduced-read-budget.ts +462 -0
  144. package/skills/guide/SKILL.md +97 -12
  145. package/skills/guide/references/operator-api.md +8 -1
  146. package/skills/guide/references/remote-store-protocol.md +94 -2
  147. package/skills/guide/references/remote-ticket-protocol.md +151 -9
  148. package/skills/guide/references/setup.md +33 -7
  149. package/skills/guide/references/ticket-migration.md +64 -0
  150. package/skills/tickets/SKILL.md +71 -0
  151. 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 = '3'
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
- * keeps the accumulator bounded, so it is the one a snapshot stores. */
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
  }