@defrex/autobuild 0.8.0 → 0.10.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 (182) hide show
  1. package/README.md +1 -1
  2. package/SPEC.md +323 -26
  3. package/bin/ab-build-runner.ts +8 -2
  4. package/docs/architecture.md +166 -14
  5. package/docs/assets/headline-wide.png +0 -0
  6. package/docs/configuration.md +175 -28
  7. package/docs/hosted-dispatcher.md +69 -13
  8. package/docs/operator-api.md +2 -2
  9. package/docs/remote-store-protocol.md +204 -16
  10. package/docs/remote-ticket-protocol.md +1 -1
  11. package/docs/setup.md +12 -9
  12. package/docs/website.md +14 -0
  13. package/package.json +45 -23
  14. package/packages/core/src/cli/artifact.ts +130 -6
  15. package/packages/core/src/cli/binary.ts +9 -1
  16. package/packages/core/src/cli/build-control.ts +110 -13
  17. package/packages/core/src/cli/context.ts +46 -0
  18. package/packages/core/src/cli/dashboard/facts.ts +215 -0
  19. package/packages/core/src/cli/dashboard/frame-parse.ts +223 -0
  20. package/packages/core/src/cli/dashboard/model.ts +141 -153
  21. package/packages/core/src/cli/dashboard/poll.ts +76 -12
  22. package/packages/core/src/cli/dashboard/render.ts +34 -5
  23. package/packages/core/src/cli/dispatch-frontend.ts +54 -5
  24. package/packages/core/src/cli/dispatch-process.ts +1 -13
  25. package/packages/core/src/cli/dispatch.ts +392 -75
  26. package/packages/core/src/cli/harvest.ts +122 -8
  27. package/packages/core/src/cli/help.ts +62 -9
  28. package/packages/core/src/cli/init-readiness-remote.ts +110 -0
  29. package/packages/core/src/cli/init-readiness-shared.ts +72 -0
  30. package/packages/core/src/cli/init-validation.ts +266 -222
  31. package/packages/core/src/cli/installation.ts +50 -8
  32. package/packages/core/src/cli/main.ts +70 -11
  33. package/packages/core/src/cli/mcp.ts +23 -1
  34. package/packages/core/src/cli/plugin.ts +7 -4
  35. package/packages/core/src/cli/remote-poll.ts +277 -0
  36. package/packages/core/src/cli/repo-state.ts +22 -0
  37. package/packages/core/src/cli/repository-status.ts +39 -1
  38. package/packages/core/src/cli/self-update.ts +77 -2
  39. package/packages/core/src/cli/status.ts +150 -28
  40. package/packages/core/src/cli/terminals.ts +2 -1
  41. package/packages/core/src/cli/ticket-assets.ts +247 -0
  42. package/packages/core/src/cli/ticket.ts +453 -8
  43. package/packages/core/src/cli/wait.ts +157 -41
  44. package/packages/core/src/cli/watch.ts +138 -168
  45. package/packages/core/src/config/live.ts +23 -0
  46. package/packages/core/src/config/pipeline-source.ts +388 -0
  47. package/packages/core/src/config/schema.ts +251 -19
  48. package/packages/core/src/events/catalog.ts +1 -0
  49. package/packages/core/src/events/globs.ts +47 -0
  50. package/packages/core/src/events/payloads.ts +52 -2
  51. package/packages/core/src/events/repository.ts +52 -1
  52. package/packages/core/src/events/sessions.ts +69 -10
  53. package/packages/core/src/events/workspace-remote.ts +12 -0
  54. package/packages/core/src/integration/harness.ts +115 -2
  55. package/packages/core/src/kernel/__snapshots__/reducer-registry.test.ts.snap +1369 -0
  56. package/packages/core/src/kernel/auto-merge-default.ts +157 -0
  57. package/packages/core/src/kernel/auto-merge.ts +99 -22
  58. package/packages/core/src/kernel/dispatch-settings.ts +42 -17
  59. package/packages/core/src/kernel/dispatch-status.ts +23 -0
  60. package/packages/core/src/kernel/engine.ts +9 -438
  61. package/packages/core/src/kernel/generators/build-log.ts +259 -0
  62. package/packages/core/src/kernel/generators/repository-journals.ts +206 -0
  63. package/packages/core/src/kernel/generators/session-log.ts +72 -0
  64. package/packages/core/src/kernel/harvest.ts +62 -18
  65. package/packages/core/src/kernel/incremental-contract.ts +75 -0
  66. package/packages/core/src/kernel/incremental-fixtures.ts +211 -0
  67. package/packages/core/src/kernel/incremental.ts +53 -0
  68. package/packages/core/src/kernel/log-index.ts +620 -0
  69. package/packages/core/src/kernel/phases.ts +16 -2
  70. package/packages/core/src/kernel/pr-attachments.ts +164 -46
  71. package/packages/core/src/kernel/reducer-registry.ts +164 -0
  72. package/packages/core/src/kernel/reducer.ts +254 -73
  73. package/packages/core/src/operator/annotations.ts +6 -0
  74. package/packages/core/src/operator/control.ts +19 -5
  75. package/packages/core/src/operator/index.ts +71 -6
  76. package/packages/core/src/operator/query.ts +253 -24
  77. package/packages/core/src/operator/read-view.ts +98 -0
  78. package/packages/core/src/operator/registry.ts +55 -10
  79. package/packages/core/src/operator/requests.ts +9 -6
  80. package/packages/core/src/operator/sandbox-host.ts +140 -0
  81. package/packages/core/src/operator/sandbox.ts +247 -14
  82. package/packages/core/src/operator/ticket-source-backend.ts +47 -0
  83. package/packages/core/src/orchestrator/approvals.ts +115 -0
  84. package/packages/core/src/orchestrator/conversation.ts +167 -0
  85. package/packages/core/src/orchestrator/model.ts +86 -0
  86. package/packages/core/src/orchestrator/prompt.ts +61 -0
  87. package/packages/core/src/orchestrator/tools.ts +117 -0
  88. package/packages/core/src/orchestrator/turn-runner.ts +457 -0
  89. package/packages/core/src/plugin-sdk/index.ts +51 -1
  90. package/packages/core/src/plugins/load.ts +135 -7
  91. package/packages/core/src/plugins/manifest.ts +183 -10
  92. package/packages/core/src/plugins/registry.ts +73 -4
  93. package/packages/core/src/ports/forge/create.ts +1 -1
  94. package/packages/core/src/ports/forge/fake.ts +31 -0
  95. package/packages/core/src/ports/forge/github-transport.ts +13 -2
  96. package/packages/core/src/ports/forge/github.ts +13 -2
  97. package/packages/core/src/ports/forge/local-git.ts +31 -1
  98. package/packages/core/src/ports/runner/claude.ts +2 -2
  99. package/packages/core/src/ports/runner/codex.ts +6 -2
  100. package/packages/core/src/ports/runner/fake.ts +7 -1
  101. package/packages/core/src/ports/runner/pi.ts +8 -6
  102. package/packages/core/src/ports/runner/provider-error.ts +13 -3
  103. package/packages/core/src/ports/tickets/fake.ts +5 -1
  104. package/packages/core/src/ports/tickets/file.ts +169 -48
  105. package/packages/core/src/ports/tickets/{remote-index.ts → hosted-index.ts} +3 -2
  106. package/packages/core/src/ports/tickets/linear.ts +33 -1
  107. package/packages/core/src/ports/tickets/operations.ts +1 -1
  108. package/packages/core/src/ports/tickets/remote.ts +8 -1
  109. package/packages/core/src/ports/types.ts +49 -3
  110. package/packages/core/src/ports/workspace/builtin-capabilities.ts +114 -0
  111. package/packages/core/src/ports/workspace/create.ts +74 -39
  112. package/packages/core/src/ports/workspace/distribution-archive.ts +171 -12
  113. package/packages/core/src/ports/workspace/fake.ts +169 -4
  114. package/packages/core/src/ports/workspace/git-worktree.ts +246 -26
  115. package/packages/core/src/ports/workspace/github-origin.ts +36 -0
  116. package/packages/core/src/ports/workspace/operator-sandbox.ts +7 -1
  117. package/packages/core/src/ports/workspace/provider-capabilities.ts +171 -0
  118. package/packages/core/src/ports/workspace/vercel-capabilities.ts +110 -0
  119. package/packages/core/src/ports/workspace/vercel-sandbox.ts +142 -114
  120. package/packages/core/src/processes/build-child.ts +17 -2
  121. package/packages/core/src/processes/build-execution-state.ts +68 -15
  122. package/packages/core/src/processes/build-runner.ts +238 -93
  123. package/packages/core/src/processes/dispatcher-selectors.ts +123 -0
  124. package/packages/core/src/processes/dispatcher.ts +501 -201
  125. package/packages/core/src/processes/execution-settlement.ts +72 -39
  126. package/packages/core/src/processes/harvest-child.ts +5 -0
  127. package/packages/core/src/processes/harvest-execution-state.ts +41 -29
  128. package/packages/core/src/processes/harvest-runner.ts +66 -19
  129. package/packages/core/src/processes/harvest.ts +196 -24
  130. package/packages/core/src/processes/infrastructure-failure-budget.ts +31 -18
  131. package/packages/core/src/processes/orchestrator-tick.ts +360 -0
  132. package/packages/core/src/processes/publication-state.ts +164 -52
  133. package/packages/core/src/processes/repo-view.ts +1436 -0
  134. package/packages/core/src/processes/sandbox-state.ts +36 -2
  135. package/packages/core/src/processes/tick-state.ts +33 -0
  136. package/packages/core/src/store/adapter.ts +41 -0
  137. package/packages/core/src/store/build-scope.ts +90 -0
  138. package/packages/core/src/store/contract.ts +1205 -0
  139. package/packages/core/src/store/digest.ts +102 -0
  140. package/packages/core/src/store/local/schema.ts +46 -2
  141. package/packages/core/src/store/local/store.ts +445 -4
  142. package/packages/core/src/store/memory.ts +246 -0
  143. package/packages/core/src/store/new-build.ts +17 -0
  144. package/packages/core/src/store/phase-session.ts +94 -0
  145. package/packages/core/src/store/remote/client.ts +310 -2
  146. package/packages/core/src/store/remote/index.ts +14 -2
  147. package/packages/core/src/store/remote/protocol.ts +115 -12
  148. package/packages/core/src/store/remote/version.ts +6 -2
  149. package/packages/core/src/store/repo-state-events.ts +205 -0
  150. package/packages/core/src/store/retention.ts +7 -0
  151. package/packages/core/src/store/session-handle.ts +94 -0
  152. package/packages/core/src/{sessions/reducer.ts → store/session-reducer.ts} +79 -19
  153. package/packages/core/src/store/snapshots.ts +88 -0
  154. package/packages/core/src/store/streams/session-writer.ts +42 -2
  155. package/packages/core/src/store/streams/wait.ts +4 -5
  156. package/packages/core/src/store/ticket-assets.ts +458 -0
  157. package/packages/core/src/store/types.ts +160 -1
  158. package/packages/core/src/testing/index.ts +59 -0
  159. package/skills/code-review/SKILL.md +5 -1
  160. package/skills/guide/SKILL.md +152 -40
  161. package/skills/guide/references/operator-api.md +2 -2
  162. package/skills/guide/references/plugin-authoring.md +17 -0
  163. package/skills/guide/references/remote-store-protocol.md +204 -16
  164. package/skills/guide/references/remote-ticket-protocol.md +1 -1
  165. package/skills/guide/references/setup.md +12 -9
  166. package/skills/implement/SKILL.md +3 -0
  167. package/skills/operate/SKILL.md +126 -0
  168. package/skills/plan/SKILL.md +4 -0
  169. package/skills/plan-review/SKILL.md +5 -1
  170. package/skills/spec/SKILL.md +46 -3
  171. package/skills/tickets/SKILL.md +57 -5
  172. package/packages/core/src/cli/dashboard/frame-image.ts +0 -373
  173. package/packages/core/src/cli/store-opening.contract.ts +0 -289
  174. package/packages/core/src/cli/testkit.ts +0 -194
  175. package/packages/core/src/markdown.ts +0 -67
  176. package/packages/core/src/operator/client.ts +0 -311
  177. package/packages/core/src/operator/server.ts +0 -616
  178. package/packages/core/src/operator/sessions.ts +0 -223
  179. package/packages/core/src/ports/runner/live-contract-fixture.ts +0 -176
  180. package/packages/core/src/ports/tickets/remote-server.ts +0 -185
  181. package/packages/core/src/store/remote/server.ts +0 -923
  182. package/packages/core/src/testing/store-failures.ts +0 -32
@@ -1,289 +0,0 @@
1
- /**
2
- * Shared behavioral contract for finite sessionless commands that own a
3
- * BuildStore. Register this against distinct command shells so selection,
4
- * credentials, production composition, and cleanup cannot drift per command.
5
- */
6
- import { describe, expect, test } from 'bun:test'
7
- import { existsSync } from 'node:fs'
8
- import { mkdtemp, rm } from 'node:fs/promises'
9
- import { tmpdir } from 'node:os'
10
- import { join } from 'node:path'
11
- import type { Exec } from '../ports/workspace/git-worktree'
12
- import { MemoryBuildStore } from '../store/memory'
13
- import { startStoreServer } from '../store/remote/server'
14
- import { mintToken } from '../store/remote/token'
15
- import type { BuildStore } from '../store/types'
16
- import type { StoreOpener } from './store-opening'
17
-
18
- const MAIN_REPO = '/main/repo'
19
- const LINKED_REPO = '/linked/worktree'
20
-
21
- const mainRepoExec: Exec = async (cmd) =>
22
- cmd[1] === 'remote'
23
- ? // No origin remote: identity falls back to the resolved checkout path.
24
- { stdout: '', stderr: "error: No such remote 'origin'\n", exitCode: 2 }
25
- : {
26
- stdout: `${MAIN_REPO}/.git\n${MAIN_REPO}/.git\n${MAIN_REPO}\n`,
27
- stderr: '',
28
- exitCode: 0,
29
- }
30
-
31
- const noGit: Exec = async () => ({
32
- stdout: '',
33
- stderr: 'not a git repository',
34
- exitCode: 128,
35
- })
36
-
37
- export interface StoreOpeningCommandOpts {
38
- targetRepo: string
39
- env: Record<string, string | undefined>
40
- exec: Exec
41
- stdout: (line: string) => void
42
- storeRef?: string
43
- openStore?: StoreOpener
44
- }
45
-
46
- export interface StoreOpeningCommandAdapter {
47
- run: (opts: StoreOpeningCommandOpts) => Promise<void>
48
- /** Extract a command-specific marker proving it used canonical main-repo
49
- * identity (for example a filtered build slug or a projected repo path). */
50
- canonicalMarker: (stdout: string[]) => string | undefined
51
- expectedCanonicalMarker: string
52
- }
53
-
54
- function trackedStore(
55
- store: MemoryBuildStore,
56
- opts: { failOperations?: boolean; onClose: () => void },
57
- ): BuildStore {
58
- return new Proxy(store, {
59
- get(target, prop) {
60
- if (prop === 'close') {
61
- return async () => opts.onClose()
62
- }
63
- const value = Reflect.get(target, prop, target) as unknown
64
- if (typeof value !== 'function') return value
65
- if (opts.failOperations === true) {
66
- return async () => {
67
- throw new Error('store operation failed')
68
- }
69
- }
70
- return value.bind(target) as unknown
71
- },
72
- }) as unknown as BuildStore
73
- }
74
-
75
- async function capture(
76
- adapter: StoreOpeningCommandAdapter,
77
- opts: Omit<StoreOpeningCommandOpts, 'stdout'>,
78
- ): Promise<string[]> {
79
- const stdout: string[] = []
80
- await adapter.run({ ...opts, stdout: (line) => stdout.push(line) })
81
- return stdout
82
- }
83
-
84
- export function describeStoreOpeningContract(
85
- name: string,
86
- adapter: StoreOpeningCommandAdapter,
87
- ): void {
88
- describe(`sessionless store-opening contract: ${name}`, () => {
89
- test('shares reference precedence, normalization, URL preservation, and opaque token forwarding', async () => {
90
- const store = new MemoryBuildStore()
91
- const opened: Array<{ ref: string; token?: string }> = []
92
- const openStore: StoreOpener = (ref, token) => {
93
- opened.push({ ref, ...(token !== undefined ? { token } : {}) })
94
- return store
95
- }
96
- const base = {
97
- targetRepo: LINKED_REPO,
98
- exec: mainRepoExec,
99
- openStore,
100
- }
101
-
102
- await capture(adapter, {
103
- ...base,
104
- env: { AB_STORE: 'environment' },
105
- storeRef: 'flag/../selected',
106
- })
107
- await capture(adapter, {
108
- ...base,
109
- env: { AB_STORE: 'environment/../selected-env' },
110
- })
111
- await capture(adapter, {
112
- ...base,
113
- env: { AB_STORE: '/absolute/state' },
114
- storeRef: ' ',
115
- })
116
- await capture(adapter, {
117
- ...base,
118
- env: { AB_STORE: ' \t' },
119
- storeRef: '',
120
- })
121
- await capture(adapter, {
122
- ...base,
123
- env: {},
124
- storeRef: 'http://store.example.test/api',
125
- })
126
- await capture(adapter, {
127
- ...base,
128
- env: {},
129
- storeRef: 'https://store.example.test/api?x=1',
130
- })
131
- await capture(adapter, {
132
- ...base,
133
- env: { AB_TOKEN: '' },
134
- storeRef: 'https://store.example.test/empty-token',
135
- })
136
- await capture(adapter, {
137
- ...base,
138
- env: { AB_TOKEN: ' opaque token ' },
139
- storeRef: 'https://store.example.test/opaque-token',
140
- })
141
-
142
- expect(opened).toEqual([
143
- { ref: `${MAIN_REPO}/selected` },
144
- { ref: `${MAIN_REPO}/selected-env` },
145
- { ref: '/absolute/state' },
146
- { ref: `${MAIN_REPO}/.autobuild` },
147
- { ref: 'http://store.example.test/api' },
148
- { ref: 'https://store.example.test/api?x=1' },
149
- { ref: 'https://store.example.test/empty-token' },
150
- {
151
- ref: 'https://store.example.test/opaque-token',
152
- token: ' opaque token ',
153
- },
154
- ])
155
- })
156
-
157
- test('uses canonical main-repository identity', async () => {
158
- const store = new MemoryBuildStore()
159
- await store.createBuild({ slug: 'main-build', repo: MAIN_REPO })
160
- await store.createBuild({ slug: 'other-build', repo: '/other/repo' })
161
- await store.ensureRepo(MAIN_REPO)
162
- const stdout = await capture(adapter, {
163
- targetRepo: LINKED_REPO,
164
- env: {},
165
- exec: mainRepoExec,
166
- openStore: () => store,
167
- })
168
- expect(adapter.canonicalMarker(stdout)).toBe(adapter.expectedCanonicalMarker)
169
- })
170
-
171
- test('closes exactly once on success, store failure, and output failure', async () => {
172
- const run = async (mode: 'success' | 'store' | 'output'): Promise<number> => {
173
- const store = new MemoryBuildStore()
174
- let closes = 0
175
- const tracked = trackedStore(store, {
176
- failOperations: mode === 'store',
177
- onClose: () => {
178
- closes += 1
179
- },
180
- })
181
- const command = adapter.run({
182
- targetRepo: LINKED_REPO,
183
- env: {},
184
- exec: mainRepoExec,
185
- openStore: () => tracked,
186
- stdout:
187
- mode === 'output'
188
- ? () => {
189
- throw new Error('output failed')
190
- }
191
- : () => {},
192
- })
193
- if (mode === 'success') await command
194
- else {
195
- await expect(command).rejects.toThrow(
196
- mode === 'store' ? 'store operation failed' : 'output failed',
197
- )
198
- }
199
- return closes
200
- }
201
-
202
- expect(await run('success')).toBe(1)
203
- expect(await run('store')).toBe(1)
204
- expect(await run('output')).toBe(1)
205
- })
206
-
207
- test('propagates opener failure without phantom cleanup', async () => {
208
- let closes = 0
209
- const unopened = new MemoryBuildStore()
210
- unopened.close = async () => {
211
- closes += 1
212
- }
213
- await expect(
214
- adapter.run({
215
- targetRepo: LINKED_REPO,
216
- env: {},
217
- exec: mainRepoExec,
218
- stdout: () => {},
219
- openStore: () => {
220
- throw new Error('open failed')
221
- },
222
- }),
223
- ).rejects.toThrow('open failed')
224
- expect(closes).toBe(0)
225
- })
226
-
227
- test('production composition opens a local SQLite root', async () => {
228
- const repo = await mkdtemp(join(tmpdir(), 'ab-store-opening-local-'))
229
- try {
230
- await adapter.run({
231
- targetRepo: repo,
232
- env: {},
233
- exec: noGit,
234
- stdout: () => {},
235
- storeRef: 'state',
236
- })
237
- expect(existsSync(join(repo, 'state', 'autobuild.sqlite'))).toBe(true)
238
- } finally {
239
- await rm(repo, { recursive: true, force: true })
240
- }
241
- })
242
-
243
- test('production composition forwards a token to remote HTTP and rejects missing or invalid credentials', async () => {
244
- const secret = 'store-opening-secret'
245
- const now = new Date('2026-07-15T12:00:00.000Z')
246
- const backing = new MemoryBuildStore({ clock: () => now })
247
- const server = startStoreServer({
248
- store: backing,
249
- secret,
250
- clock: () => now,
251
- })
252
- const token = mintToken(secret, {
253
- build: '*',
254
- session: '*',
255
- exp: now.getTime() + 60_000,
256
- })
257
- try {
258
- await adapter.run({
259
- targetRepo: LINKED_REPO,
260
- env: { AB_TOKEN: token },
261
- exec: mainRepoExec,
262
- stdout: () => {},
263
- storeRef: server.url,
264
- })
265
- await expect(
266
- adapter.run({
267
- targetRepo: LINKED_REPO,
268
- env: {},
269
- exec: mainRepoExec,
270
- stdout: () => {},
271
- storeRef: server.url,
272
- }),
273
- ).rejects.toThrow(/missing bearer token/)
274
- await expect(
275
- adapter.run({
276
- targetRepo: LINKED_REPO,
277
- env: { AB_TOKEN: 'invalid-token' },
278
- exec: mainRepoExec,
279
- stdout: () => {},
280
- storeRef: server.url,
281
- }),
282
- ).rejects.toThrow(/invalid or expired token/)
283
- } finally {
284
- await server.stop()
285
- await backing.close()
286
- }
287
- })
288
- })
289
- }
@@ -1,194 +0,0 @@
1
- /**
2
- * Test-support fixtures for the CLI suite — imported only by the colocated
3
- * `*.test.ts` files, never by production code (mirrors the src/testing rule).
4
- *
5
- * The store fixture is a seeded MemoryBuildStore (the reference adapter — its
6
- * behavior IS the contract, store/contract.ts) and the git fixtures are real
7
- * throwaway repos, matching the patterns in git-worktree.test.ts.
8
- */
9
- import { mkdir, writeFile } from 'node:fs/promises'
10
- import { join } from 'node:path'
11
- import type { EventEnvelope, EventWrite } from '../events/catalog'
12
- import { DISPATCHER } from '../events/envelope'
13
- import type { EventType } from '../events/payloads'
14
- import type { PrImageHostTarget } from '../ontology'
15
- import { sequentialIds } from '../ids'
16
- import { FakeForge } from '../ports/forge/fake'
17
- import { spawnExec } from '../ports/workspace/git-worktree'
18
- import { MemoryBuildStore } from '../store/memory'
19
- import type { BuildStore } from '../store/types'
20
- import { steppingClock } from '../testing/fixed'
21
- import type { CliEnv } from './env'
22
- import type { CliDeps } from './main'
23
-
24
- export const BUILD = 'auth-rate-limit'
25
- export const BRANCH = 'ab/auth-rate-limit'
26
-
27
- export function makeEnv(overrides: Partial<CliEnv> = {}): CliEnv {
28
- return {
29
- store: '/tmp/ab-store',
30
- build: BUILD,
31
- phase: 'implement',
32
- round: 1,
33
- session: 's_test',
34
- ...overrides,
35
- }
36
- }
37
-
38
- /** Build + `build.created` + spec@0 (`spec.imported`) — every phase's floor. */
39
- export async function seedStore(
40
- opts: { imageHost?: PrImageHostTarget } = {},
41
- ): Promise<MemoryBuildStore> {
42
- const store = new MemoryBuildStore({ clock: steppingClock() })
43
- await store.createBuild({ slug: BUILD, repo: 'acme/app', branch: BRANCH })
44
- await store.append(BUILD, {
45
- actor: DISPATCHER,
46
- type: 'build.created',
47
- payload: {
48
- ticket: {
49
- source: 'linear',
50
- id: 'ENG-42',
51
- url: 'https://linear.app/acme/issue/ENG-42',
52
- title: 'Auth rate limiting',
53
- },
54
- repo: 'acme/app',
55
- baseBranch: 'main',
56
- ...(opts.imageHost !== undefined ? { pr: { imageHost: opts.imageHost } } : {}),
57
- },
58
- })
59
- await store.putArtifact(BUILD, {
60
- kind: 'spec',
61
- content: '# Spec: auth rate limiting\n',
62
- })
63
- await store.append(BUILD, {
64
- actor: DISPATCHER,
65
- type: 'spec.imported',
66
- payload: {
67
- artifact: { kind: 'spec', rev: 0 },
68
- ticket: { source: 'linear', id: 'ENG-42' },
69
- },
70
- })
71
- return store
72
- }
73
-
74
- export interface TestDeps extends CliDeps {
75
- forge: FakeForge
76
- out: string[]
77
- err: string[]
78
- }
79
-
80
- export function makeDeps(opts: {
81
- store: MemoryBuildStore
82
- env: CliEnv
83
- workspacePath?: string
84
- forge?: FakeForge
85
- }): TestDeps {
86
- const out: string[] = []
87
- const err: string[] = []
88
- const forge = opts.forge ?? new FakeForge()
89
- return {
90
- store: opts.store,
91
- env: opts.env,
92
- workspacePath: opts.workspacePath ?? '/nonexistent-workspace',
93
- forge,
94
- exec: spawnExec,
95
- ids: sequentialIds(),
96
- clock: steppingClock(),
97
- stdout: (line) => out.push(line),
98
- stderr: (line) => err.push(line),
99
- out,
100
- err,
101
- }
102
- }
103
-
104
- /**
105
- * Wrap a store so ONE build's compare-and-set append fails outright, to drive
106
- * the bulk walk's partial-progress path. Prototype-proxy idiom, as
107
- * `withRacingAppend` in bulk-control.test.ts; every other build and every other
108
- * operation delegates untouched, so the walk's real bookkeeping is what the
109
- * failure report is built from.
110
- */
111
- export function withFailingAppend(store: MemoryBuildStore, slug: string, error: Error): BuildStore {
112
- const proxy = Object.create(store) as MemoryBuildStore
113
- proxy.appendIfCurrent = async <T extends EventType>(
114
- target: string,
115
- expectedSeq: number,
116
- event: EventWrite<T>,
117
- ): Promise<EventEnvelope<T> | null> => {
118
- if (target === slug) throw error
119
- return store.appendIfCurrent(target, expectedSeq, event)
120
- }
121
- return proxy
122
- }
123
-
124
- // ── Real-git fixtures (identity pinned per invocation, like git-worktree.test) ─
125
-
126
- export const GIT_ID = [
127
- '-c',
128
- 'user.email=ab@test.invalid',
129
- '-c',
130
- 'user.name=ab-test',
131
- '-c',
132
- 'commit.gpgsign=false',
133
- ]
134
-
135
- export async function runGit(args: string[], cwd: string): Promise<string> {
136
- const result = await spawnExec(['git', ...args], { cwd })
137
- if (result.exitCode !== 0) {
138
- throw new Error(`git ${args.join(' ')} failed: ${result.stderr}`)
139
- }
140
- return result.stdout.trim()
141
- }
142
-
143
- /**
144
- * A workspace repo on branch `ab/auth-rate-limit`, branched off `main`.
145
- * Deliberately does NOT gitignore `.ab/` — real repos don't either (§7:
146
- * establishing the gitignored scratch dir is the PRODUCT's job, via the
147
- * self-excluding `.ab/.gitignore` that `ab context` writes); a fixture that
148
- * hand-wrote the ignore would hide a regression in that mechanism.
149
- */
150
- export async function initWorkspaceRepo(dir: string, branch = BRANCH): Promise<void> {
151
- await mkdir(dir, { recursive: true })
152
- await runGit(['init', '-q', '-b', 'main'], dir)
153
- await writeFile(join(dir, 'README.md'), 'fixture\n')
154
- await runGit(['add', '.'], dir)
155
- await runGit([...GIT_ID, 'commit', '-q', '-m', 'initial'], dir)
156
- await runGit(['checkout', '-q', '-b', branch], dir)
157
- }
158
-
159
- /**
160
- * Attach a throwaway bare origin, seed its build ref at the local branch tip,
161
- * and seed `main` at an independently selected target commit. Directly moving
162
- * the bare target ref deliberately leaves operator-owned local/tracking refs
163
- * untouched, matching the real workspace boundary tests.
164
- */
165
- export async function initBareOrigin(
166
- workspace: string,
167
- remote: string,
168
- branch = BRANCH,
169
- target = 'HEAD',
170
- ): Promise<void> {
171
- const targetSha = await runGit(['rev-parse', '--verify', `${target}^{commit}`], workspace)
172
- await mkdir(remote, { recursive: true })
173
- await runGit(['init', '-q', '--bare'], remote)
174
- await runGit(['remote', 'add', 'origin', remote], workspace)
175
- await runGit(['push', '-q', 'origin', `${branch}:refs/heads/${branch}`], workspace)
176
- await runGit(['update-ref', 'refs/heads/main', targetSha], remote)
177
- await runGit(['symbolic-ref', 'HEAD', 'refs/heads/main'], remote)
178
- }
179
-
180
- export function remoteBranchHead(remote: string, branch = BRANCH): Promise<string> {
181
- return runGit(['rev-parse', `refs/heads/${branch}`], remote)
182
- }
183
-
184
- export async function commitFile(
185
- dir: string,
186
- file: string,
187
- content: string,
188
- message: string,
189
- ): Promise<string> {
190
- await writeFile(join(dir, file), content)
191
- await runGit(['add', file], dir)
192
- await runGit([...GIT_ID, 'commit', '-q', '-m', message], dir)
193
- return runGit(['rev-parse', 'HEAD'], dir)
194
- }
@@ -1,67 +0,0 @@
1
- /**
2
- * Approximate Markdown reference extraction, shared by the checks that ask
3
- * "does this link point at something that exists?".
4
- *
5
- * These are regexes, not a CommonMark parser. Reference-style targets
6
- * (`![alt][ref]`) and targets containing parentheses are invisible to them.
7
- * Both callers are link-existence checks, where an unseen reference fails
8
- * toward a loud complaint rather than a silent pass, so the approximation is
9
- * safe — but a false report is a signal to widen the parser, never to delete
10
- * the thing it failed to see.
11
- */
12
-
13
- /**
14
- * Blanks out fenced code blocks, preserving line count. A target inside a fence
15
- * is sample text, not a rendered reference. Backtick and tilde fences are
16
- * tracked separately so a `~~~` line inside a ``` block does not close it.
17
- */
18
- export function withoutFencedCode(markdown: string): string {
19
- const lines = markdown.split('\n')
20
- let fence: '`' | '~' | undefined
21
- return lines
22
- .map((line) => {
23
- const opening = line.match(/^\s*(`{3,}|~{3,})/)
24
- if (opening) {
25
- const marker = opening[1]![0] as '`' | '~'
26
- if (fence === undefined) fence = marker
27
- else if (fence === marker) fence = undefined
28
- return ''
29
- }
30
- return fence === undefined ? line : ''
31
- })
32
- .join('\n')
33
- }
34
-
35
- /**
36
- * Inline link and image targets, in source order. Angle brackets are unwrapped
37
- * and a title suffix is dropped, so `[a](<b.md> "T")` yields `b.md`.
38
- */
39
- export function markdownTargets(markdown: string): string[] {
40
- const targets: string[] = []
41
- for (const match of markdown.matchAll(/!?\[[^\]]*\]\(([^)]+)\)/g)) {
42
- const inline = match[1]!.trim()
43
- // An angle-bracketed destination runs to its closing `>`, and whatever
44
- // follows is the title. The two must be read together: unwrapping first
45
- // fails whenever a title is present (the text no longer ends in `>`), and
46
- // splitting the title off first would truncate a bracketed destination that
47
- // legitimately contains whitespace and a quote. A `<` with no closing `>`
48
- // is malformed, and falls through to the unbracketed reading.
49
- const closing = inline.startsWith('<') ? inline.indexOf('>') : -1
50
- targets.push(closing === -1 ? inline.split(/\s+["']/u, 1)[0]! : inline.slice(1, closing))
51
- }
52
- return targets
53
- }
54
-
55
- /**
56
- * `src` attributes of raw `<img>` tags, in source order. Kept separate from
57
- * `markdownTargets` so callers that only care about Markdown syntax keep their
58
- * existing behaviour: Markdown permits raw HTML, and this repository's README
59
- * already mixes it in, so a centred hero image would otherwise be invisible.
60
- */
61
- export function htmlImageTargets(markdown: string): string[] {
62
- const targets: string[] = []
63
- for (const match of markdown.matchAll(/<img\b[^>]*?\ssrc\s*=\s*("([^"]*)"|'([^']*)')/giu)) {
64
- targets.push((match[2] ?? match[3])!.trim())
65
- }
66
- return targets
67
- }