@wringer/core 0.1.0 → 0.1.2

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 (258) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +71 -2
  3. package/dist/answer-supervisor-connection.d.ts +8 -0
  4. package/dist/app-machine.d.ts +76 -0
  5. package/dist/append-run-event.d.ts +16 -0
  6. package/dist/bind-drivers.d.ts +9 -0
  7. package/dist/build-commands.d.ts +5 -0
  8. package/dist/build-envelope-json-schema.d.ts +1 -0
  9. package/dist/build-envelope.d.ts +16 -0
  10. package/dist/build-launch-guard.d.ts +1 -0
  11. package/dist/build-manifest-write.d.ts +6 -0
  12. package/dist/build-promised-object-reads.d.ts +7 -0
  13. package/dist/build-report.d.ts +10 -0
  14. package/dist/build-run-handle.d.ts +2 -0
  15. package/dist/build-run-view.d.ts +29 -0
  16. package/dist/build-service-key.d.ts +1 -0
  17. package/dist/build-shut-down-steps.d.ts +14 -0
  18. package/dist/build-skip-worktree-marks.d.ts +7 -0
  19. package/dist/build-spawn-error.d.ts +2 -0
  20. package/dist/build-state-paths.d.ts +21 -0
  21. package/dist/build-submodule-adds.d.ts +7 -0
  22. package/dist/builtins/build-doctor-command.d.ts +2 -0
  23. package/dist/builtins/build-exec-command.d.ts +2 -0
  24. package/dist/builtins/build-lifecycle-commands.d.ts +3 -0
  25. package/dist/builtins/build-state-commands.d.ts +2 -0
  26. package/dist/builtins/build-use-pointers.d.ts +9 -0
  27. package/dist/builtins/build-verdict-commands.d.ts +2 -0
  28. package/dist/builtins/call-run-supervisor.d.ts +5 -0
  29. package/dist/builtins/pick-doctor-outcome.d.ts +13 -0
  30. package/dist/builtins/pick-start-action.d.ts +10 -0
  31. package/dist/builtins/pick-stop-plan.d.ts +14 -0
  32. package/dist/builtins/start-run.d.ts +22 -0
  33. package/dist/builtins/stop-data-schema.d.ts +6 -0
  34. package/dist/builtins/stop-run.d.ts +3 -0
  35. package/dist/call-supervisor.d.ts +10 -0
  36. package/dist/cap-data.d.ts +17 -0
  37. package/dist/collect-driver-configs.d.ts +7 -0
  38. package/dist/create-directory.d.ts +1 -0
  39. package/dist/create-evidence-writer.d.ts +13 -0
  40. package/dist/create-file-state.d.ts +36 -0
  41. package/dist/create-log-writer.d.ts +5 -0
  42. package/dist/create-node-repo-host.d.ts +2 -0
  43. package/dist/create-node-state-file-system.d.ts +10 -0
  44. package/dist/create-repo-state.d.ts +2 -0
  45. package/dist/create-socket-folder.d.ts +1 -0
  46. package/dist/create-wringer.d.ts +17 -0
  47. package/dist/decide-guard.d.ts +12 -0
  48. package/dist/define-driver.d.ts +13 -0
  49. package/dist/dispatch.d.ts +10 -0
  50. package/dist/envelope.d.ts +63 -0
  51. package/dist/error-codes.d.ts +25 -0
  52. package/dist/find-cancel-failure.d.ts +2 -0
  53. package/dist/find-colliding-service-key.d.ts +5 -0
  54. package/dist/find-own-value.d.ts +1 -0
  55. package/dist/find-remote-stop-failure.d.ts +2 -0
  56. package/dist/find-verdict-refusal.d.ts +3 -0
  57. package/dist/follow-process.d.ts +10 -0
  58. package/dist/get-lock-value.d.ts +2 -0
  59. package/dist/get-target.d.ts +2 -0
  60. package/dist/host.d.ts +19 -0
  61. package/dist/index.d.ts +60 -0
  62. package/dist/is-error-code.d.ts +1 -0
  63. package/dist/keep-cancelled-call.d.ts +1 -0
  64. package/dist/kill-process-group.d.ts +1 -0
  65. package/dist/launch-line-schema.d.ts +3 -0
  66. package/dist/listen-supervisor-socket.d.ts +8 -0
  67. package/dist/local.d.ts +2 -0
  68. package/dist/make-fence.d.ts +8 -0
  69. package/dist/make-fenced-exec.d.ts +10 -0
  70. package/dist/make-process-supervisor-port.d.ts +7 -0
  71. package/dist/open-drive.d.ts +23 -0
  72. package/dist/open-runless-drive.d.ts +17 -0
  73. package/dist/parse-launch-line.d.ts +2 -0
  74. package/dist/parse-patch-id.d.ts +2 -0
  75. package/dist/parse-report-block.d.ts +9 -0
  76. package/dist/parse-run-log.d.ts +2 -0
  77. package/dist/pick-access-refusal.d.ts +12 -0
  78. package/dist/pick-command-needs.d.ts +15 -0
  79. package/dist/pick-command-plan.d.ts +23 -0
  80. package/dist/pick-context-target.d.ts +7 -0
  81. package/dist/pick-drive-kind.d.ts +8 -0
  82. package/dist/pick-envelope-context.d.ts +1 -0
  83. package/dist/pick-envelope-payload.d.ts +12 -0
  84. package/dist/pick-envelope-run.d.ts +6 -0
  85. package/dist/pick-envelope-target.d.ts +4 -0
  86. package/dist/pick-late-failure-write.d.ts +13 -0
  87. package/dist/pick-log-refusal.d.ts +4 -0
  88. package/dist/pick-merge-base-failure.d.ts +2 -0
  89. package/dist/pick-merge-base-steps.d.ts +13 -0
  90. package/dist/pick-pending-cleanups.d.ts +10 -0
  91. package/dist/pick-submodule-refusal.d.ts +2 -0
  92. package/dist/pick-supervisor-operation.d.ts +16 -0
  93. package/dist/prepare-args.d.ts +3 -0
  94. package/dist/probe-process.d.ts +1 -0
  95. package/dist/quote-shell-word.d.ts +1 -0
  96. package/dist/race-deadline.d.ts +1 -0
  97. package/dist/read-changed-paths.d.ts +7 -0
  98. package/dist/read-default-base.d.ts +2 -0
  99. package/dist/read-fingerprint.d.ts +13 -0
  100. package/dist/read-head-sha.d.ts +2 -0
  101. package/dist/read-launch-host.d.ts +26 -0
  102. package/dist/read-pid-namespace.d.ts +1 -0
  103. package/dist/read-stream-line-batches.d.ts +2 -0
  104. package/dist/read-stream-lines.d.ts +2 -0
  105. package/dist/read-text.d.ts +1 -0
  106. package/dist/read-verdicts.d.ts +5 -0
  107. package/dist/remove-directory.d.ts +1 -0
  108. package/dist/remove-file.d.ts +1 -0
  109. package/dist/render-report-markdown.d.ts +4 -0
  110. package/dist/report-schema.d.ts +51 -0
  111. package/dist/run-events.d.ts +129 -0
  112. package/dist/run-git.d.ts +9 -0
  113. package/dist/run-process.d.ts +19 -0
  114. package/dist/run-supervisor-process.d.ts +2 -0
  115. package/dist/run-supervisor.d.ts +16 -0
  116. package/dist/settle-dispatch-answer.d.ts +9 -0
  117. package/dist/settle-envelope.d.ts +16 -0
  118. package/dist/ssh.d.ts +12 -0
  119. package/dist/start-launch.d.ts +20 -0
  120. package/dist/start-services.d.ts +16 -0
  121. package/dist/supervisor-machine.d.ts +78 -0
  122. package/dist/supervisor-protocol.d.ts +43 -0
  123. package/dist/to-wringer-error.d.ts +2 -0
  124. package/dist/types.d.ts +353 -0
  125. package/dist/wait-for-cancelled-calls.d.ts +2 -0
  126. package/dist/wringer-error.d.ts +12 -0
  127. package/dist/write-envelope-log.d.ts +11 -0
  128. package/dist/write-late-failure.d.ts +11 -0
  129. package/dist/write-new-file.d.ts +1 -0
  130. package/envelope.schema.json +175 -0
  131. package/package.json +21 -5
  132. package/src/answer-supervisor-connection.ts +117 -0
  133. package/src/app-machine.ts +174 -0
  134. package/src/append-run-event.ts +131 -0
  135. package/src/bind-drivers.ts +33 -0
  136. package/src/build-commands.ts +104 -0
  137. package/src/build-envelope-json-schema.ts +34 -0
  138. package/src/build-envelope.ts +40 -0
  139. package/src/build-launch-guard.ts +5 -0
  140. package/src/build-manifest-write.ts +26 -0
  141. package/src/build-promised-object-reads.ts +48 -0
  142. package/src/build-report.ts +55 -0
  143. package/src/build-run-handle.ts +13 -0
  144. package/src/build-run-view.ts +120 -0
  145. package/src/build-service-key.ts +4 -0
  146. package/src/build-shut-down-steps.ts +54 -0
  147. package/src/build-skip-worktree-marks.ts +36 -0
  148. package/src/build-spawn-error.ts +12 -0
  149. package/src/build-state-paths.ts +53 -0
  150. package/src/build-submodule-adds.ts +64 -0
  151. package/src/builtins/build-doctor-command.ts +163 -0
  152. package/src/builtins/build-exec-command.ts +41 -0
  153. package/src/builtins/build-lifecycle-commands.ts +115 -0
  154. package/src/builtins/build-state-commands.ts +152 -0
  155. package/src/builtins/build-use-pointers.ts +26 -0
  156. package/src/builtins/build-verdict-commands.ts +266 -0
  157. package/src/builtins/call-run-supervisor.ts +40 -0
  158. package/src/builtins/pick-doctor-outcome.ts +32 -0
  159. package/src/builtins/pick-start-action.ts +35 -0
  160. package/src/builtins/pick-stop-plan.ts +32 -0
  161. package/src/builtins/start-run.ts +281 -0
  162. package/src/builtins/stop-data-schema.ts +8 -0
  163. package/src/builtins/stop-run.ts +49 -0
  164. package/src/call-supervisor.ts +109 -0
  165. package/src/cap-data.ts +43 -0
  166. package/src/collect-driver-configs.ts +36 -0
  167. package/src/create-directory.ts +6 -0
  168. package/src/create-evidence-writer.ts +82 -0
  169. package/src/create-file-state.ts +193 -0
  170. package/src/create-log-writer.ts +38 -0
  171. package/src/create-node-repo-host.ts +20 -0
  172. package/src/create-node-state-file-system.ts +52 -0
  173. package/src/create-repo-state.ts +15 -0
  174. package/src/create-socket-folder.ts +24 -0
  175. package/src/create-wringer.ts +169 -0
  176. package/src/decide-guard.ts +47 -0
  177. package/src/define-driver.ts +165 -0
  178. package/src/dispatch.ts +589 -0
  179. package/src/envelope.ts +57 -0
  180. package/src/error-codes.ts +39 -0
  181. package/src/find-cancel-failure.ts +7 -0
  182. package/src/find-colliding-service-key.ts +16 -0
  183. package/src/find-own-value.ts +8 -0
  184. package/src/find-remote-stop-failure.ts +19 -0
  185. package/src/find-verdict-refusal.ts +26 -0
  186. package/src/follow-process.ts +147 -0
  187. package/src/get-lock-value.ts +17 -0
  188. package/src/get-target.ts +15 -0
  189. package/src/host.ts +26 -0
  190. package/src/index.ts +75 -0
  191. package/src/is-error-code.ts +3 -0
  192. package/src/keep-cancelled-call.ts +21 -0
  193. package/src/kill-process-group.ts +13 -0
  194. package/src/launch-line-schema.ts +6 -0
  195. package/src/listen-supervisor-socket.ts +75 -0
  196. package/src/local.ts +20 -0
  197. package/src/make-fence.ts +27 -0
  198. package/src/make-fenced-exec.ts +52 -0
  199. package/src/make-process-supervisor-port.ts +65 -0
  200. package/src/open-drive.ts +151 -0
  201. package/src/open-runless-drive.ts +71 -0
  202. package/src/parse-launch-line.ts +19 -0
  203. package/src/parse-patch-id.ts +8 -0
  204. package/src/parse-report-block.ts +39 -0
  205. package/src/parse-run-log.ts +34 -0
  206. package/src/pick-access-refusal.ts +32 -0
  207. package/src/pick-command-needs.ts +29 -0
  208. package/src/pick-command-plan.ts +51 -0
  209. package/src/pick-context-target.ts +22 -0
  210. package/src/pick-drive-kind.ts +23 -0
  211. package/src/pick-envelope-context.ts +10 -0
  212. package/src/pick-envelope-payload.ts +28 -0
  213. package/src/pick-envelope-run.ts +25 -0
  214. package/src/pick-envelope-target.ts +10 -0
  215. package/src/pick-late-failure-write.ts +44 -0
  216. package/src/pick-log-refusal.ts +18 -0
  217. package/src/pick-merge-base-failure.ts +15 -0
  218. package/src/pick-merge-base-steps.ts +37 -0
  219. package/src/pick-pending-cleanups.ts +23 -0
  220. package/src/pick-submodule-refusal.ts +46 -0
  221. package/src/pick-supervisor-operation.ts +52 -0
  222. package/src/prepare-args.ts +22 -0
  223. package/src/probe-process.ts +16 -0
  224. package/src/quote-shell-word.ts +9 -0
  225. package/src/race-deadline.ts +23 -0
  226. package/src/read-changed-paths.ts +39 -0
  227. package/src/read-default-base.ts +21 -0
  228. package/src/read-fingerprint.ts +271 -0
  229. package/src/read-head-sha.ts +23 -0
  230. package/src/read-launch-host.ts +64 -0
  231. package/src/read-pid-namespace.ts +18 -0
  232. package/src/read-stream-line-batches.ts +28 -0
  233. package/src/read-stream-lines.ts +11 -0
  234. package/src/read-text.ts +5 -0
  235. package/src/read-verdicts.ts +57 -0
  236. package/src/remove-directory.ts +6 -0
  237. package/src/remove-file.ts +6 -0
  238. package/src/render-report-markdown.ts +53 -0
  239. package/src/report-schema.ts +37 -0
  240. package/src/run-events.ts +71 -0
  241. package/src/run-git.ts +49 -0
  242. package/src/run-process.ts +168 -0
  243. package/src/run-supervisor-process.ts +51 -0
  244. package/src/run-supervisor.ts +327 -0
  245. package/src/settle-dispatch-answer.ts +26 -0
  246. package/src/settle-envelope.ts +34 -0
  247. package/src/ssh.ts +211 -0
  248. package/src/start-launch.ts +327 -0
  249. package/src/start-services.ts +108 -0
  250. package/src/supervisor-machine.ts +169 -0
  251. package/src/supervisor-protocol.ts +42 -0
  252. package/src/to-wringer-error.ts +20 -0
  253. package/src/types.ts +476 -0
  254. package/src/wait-for-cancelled-calls.ts +37 -0
  255. package/src/wringer-error.ts +25 -0
  256. package/src/write-envelope-log.ts +72 -0
  257. package/src/write-late-failure.ts +40 -0
  258. package/src/write-new-file.ts +17 -0
@@ -0,0 +1,151 @@
1
+ import { join } from 'node:path';
2
+ import { buildRunHandle } from './build-run-handle.ts';
3
+ import type { RunView } from './build-run-view.ts';
4
+ import { buildServiceKey } from './build-service-key.ts';
5
+ import { collectDriverConfigs } from './collect-driver-configs.ts';
6
+ import { createEvidenceWriter } from './create-evidence-writer.ts';
7
+ import type { Access } from './host.ts';
8
+ import { makeFence } from './make-fence.ts';
9
+ import { makeDriverExec, makeRepoExec } from './make-fenced-exec.ts';
10
+ import type {
11
+ Drive,
12
+ Emit,
13
+ EvidenceFiles,
14
+ OpenedDriver,
15
+ SupervisorPort,
16
+ TargetDefinition,
17
+ Wringer,
18
+ } from './types.ts';
19
+ import { WringerError } from './wringer-error.ts';
20
+
21
+ export interface DriveRequest {
22
+ readonly wringer: Wringer;
23
+ readonly root: string;
24
+ readonly stateDir: string;
25
+ readonly targetName: string;
26
+ readonly target: TargetDefinition;
27
+ readonly access: Access;
28
+ readonly hint: string;
29
+ readonly run: RunView;
30
+ readonly feature: string | null;
31
+ readonly cursors: Readonly<Record<string, number>>;
32
+ readonly signal: AbortSignal;
33
+
34
+ // When `signal` fires, in ms since the epoch on the `now` clock.
35
+ readonly deadlineAt: number;
36
+ readonly now: () => Date;
37
+ readonly emit: Emit;
38
+ readonly files: EvidenceFiles;
39
+ readonly callSupervisor: SupervisorPort['call'];
40
+ }
41
+
42
+ export const unassignedFeature = 'unassigned';
43
+
44
+ // Every client and exec handed to a command body goes through the fence for `access`, so a body
45
+ // that writes on a read-only target is refused at the call, whatever the command declared.
46
+ export function openDrive(request: DriveRequest): Drive {
47
+ const run = buildRunHandle(request.stateDir, request.run.runID);
48
+
49
+ const evidence = createEvidenceWriter({
50
+ root: request.root,
51
+ directory: join(run.paths.features, request.feature ?? unassignedFeature),
52
+ now: request.now,
53
+ files: request.files,
54
+ });
55
+
56
+ const execOptions = {
57
+ host: request.target.host,
58
+ access: request.access,
59
+ hint: request.hint,
60
+ signal: request.signal,
61
+ };
62
+
63
+ const fence = makeFence({ access: request.access, hint: request.hint });
64
+
65
+ const entries = collectDriverConfigs(request.wringer, request.target, {
66
+ target: request.targetName,
67
+ endpoints: request.run.endpoints,
68
+ run,
69
+ });
70
+
71
+ const drivers = new Map<string, OpenedDriver>(
72
+ entries.map((entry) => [
73
+ entry.name,
74
+ entry.configured.open({
75
+ target: request.targetName,
76
+ access: request.access,
77
+ endpoints: request.run.endpoints,
78
+ run,
79
+ exec: makeDriverExec(execOptions),
80
+ evidence: evidence.writer,
81
+ cursors: request.cursors,
82
+ signal: request.signal,
83
+ timeLeftMs: () => Math.max(0, request.deadlineAt - request.now().getTime()),
84
+ fence,
85
+ service: (service) => ({
86
+ call: (method, args) =>
87
+ request.callSupervisor({
88
+ socket: getSocket(request.run),
89
+ request: {
90
+ service: buildServiceKey(entry.name, service),
91
+ method,
92
+ args,
93
+ deadlineAt: request.deadlineAt,
94
+ },
95
+ signal: request.signal,
96
+ }),
97
+ }),
98
+ }),
99
+ ]),
100
+ );
101
+
102
+ const clients = Object.fromEntries(
103
+ request.wringer.drivers.map((driver) => [
104
+ driver.name,
105
+ drivers.get(driver.name)?.client ?? buildMissingClient(driver.name, request.targetName),
106
+ ]),
107
+ );
108
+
109
+ return {
110
+ drivers,
111
+ evidence: evidence.collected,
112
+ ctx: {
113
+ target: request.targetName,
114
+ access: request.access,
115
+ feature: request.feature,
116
+ endpoints: request.run.endpoints,
117
+ drivers: clients,
118
+ exec: makeRepoExec(execOptions),
119
+ evidence: evidence.writer,
120
+ signal: request.signal,
121
+ emit: request.emit,
122
+ },
123
+ };
124
+ }
125
+
126
+ function getSocket(run: RunView): string {
127
+ if (run.supervisor === null) {
128
+ throw new WringerError('not-running', {
129
+ message: 'The run has no supervisor.',
130
+ hint: 'Run `up --restart`, or `attach` again, to start a new run.',
131
+ });
132
+ }
133
+
134
+ return run.supervisor.socket;
135
+ }
136
+
137
+ // A target that gives a driver no config still gets an entry for it, so repo code typed against
138
+ // every driver reaches a clear refusal at the first call instead of reading undefined.
139
+ function buildMissingClient(driver: string, target: string): unknown {
140
+ return new Proxy(
141
+ {},
142
+ {
143
+ get: () => {
144
+ throw new WringerError('usage', {
145
+ message: `Target ${target} gives the ${driver} driver no config.`,
146
+ hint: `Add a ${driver} entry to the drivers of target ${target}.`,
147
+ });
148
+ },
149
+ },
150
+ );
151
+ }
@@ -0,0 +1,71 @@
1
+ import { join } from 'node:path';
2
+ import { createEvidenceWriter } from './create-evidence-writer.ts';
3
+ import type { Access } from './host.ts';
4
+ import { makeRepoExec } from './make-fenced-exec.ts';
5
+ import type { Drive, Emit, EvidenceFiles, TargetDefinition, Wringer } from './types.ts';
6
+ import { WringerError } from './wringer-error.ts';
7
+
8
+ export interface RunlessDriveRequest {
9
+ readonly wringer: Wringer;
10
+ readonly root: string;
11
+ readonly stateDir: string;
12
+ readonly targetName: string;
13
+ readonly target: TargetDefinition;
14
+ readonly access: Access;
15
+ readonly hint: string;
16
+ readonly feature: string | null;
17
+ readonly signal: AbortSignal;
18
+ readonly now: () => Date;
19
+ readonly emit: Emit;
20
+ readonly files: EvidenceFiles;
21
+ }
22
+
23
+ // A repo command that declares it needs no run still gets the fenced exec and an evidence folder.
24
+ // Its driver clients refuse at the first call, because no run holds their services.
25
+ export function openRunlessDrive(request: RunlessDriveRequest): Drive {
26
+ const evidence = createEvidenceWriter({
27
+ root: request.root,
28
+ directory: join(request.stateDir, 'payloads'),
29
+ now: request.now,
30
+ files: request.files,
31
+ });
32
+
33
+ const clients = Object.fromEntries(
34
+ request.wringer.drivers.map((driver) => [driver.name, buildRunlessClient(driver.name)]),
35
+ );
36
+
37
+ return {
38
+ drivers: new Map(),
39
+ evidence: evidence.collected,
40
+ ctx: {
41
+ target: request.targetName,
42
+ access: request.access,
43
+ feature: request.feature,
44
+ endpoints: {},
45
+ drivers: clients,
46
+ exec: makeRepoExec({
47
+ host: request.target.host,
48
+ access: request.access,
49
+ hint: request.hint,
50
+ signal: request.signal,
51
+ }),
52
+ evidence: evidence.writer,
53
+ signal: request.signal,
54
+ emit: request.emit,
55
+ },
56
+ };
57
+ }
58
+
59
+ function buildRunlessClient(driver: string): unknown {
60
+ return new Proxy(
61
+ {},
62
+ {
63
+ get: () => {
64
+ throw new WringerError('not-running', {
65
+ message: `The ${driver} driver needs a running run, and this command runs without one.`,
66
+ hint: 'Set `needsRun` on the command, or start a run with `up` or `attach`.',
67
+ });
68
+ },
69
+ },
70
+ );
71
+ }
@@ -0,0 +1,19 @@
1
+ import type { LaunchLine } from './launch-line-schema.ts';
2
+ import { launchLineSchema } from './launch-line-schema.ts';
3
+
4
+ // A line that holds no JSON object, such as plain text or a JSON array, reaches `ready` as its text.
5
+ export function parseLaunchLine(line: string): LaunchLine {
6
+ const parsed = launchLineSchema.safeParse(parseJSON(line));
7
+
8
+ return parsed.success ? parsed.data : { message: line };
9
+ }
10
+
11
+ function parseJSON(line: string): unknown {
12
+ try {
13
+ const parsed: unknown = JSON.parse(line);
14
+
15
+ return parsed;
16
+ } catch {
17
+ return undefined;
18
+ }
19
+ }
@@ -0,0 +1,8 @@
1
+ export const emptyFingerprint = 'empty';
2
+
3
+ // `patch-id` prints the ID and the commit it read, or nothing for an empty diff.
4
+ export function parsePatchID(stdout: string): string {
5
+ const [patchID = ''] = stdout.trim().split(' ');
6
+
7
+ return patchID === '' ? emptyFingerprint : patchID;
8
+ }
@@ -0,0 +1,39 @@
1
+ import { reportBlockClose, reportBlockOpen } from './render-report-markdown.ts';
2
+ import { reportSchema } from './report-schema.ts';
3
+ import type { Report } from './report-schema.ts';
4
+
5
+ export type ReportBlockResult =
6
+ | { readonly ok: true; readonly report: Report }
7
+ | { readonly ok: false; readonly reason: string };
8
+
9
+ export function parseReportBlock(body: string): ReportBlockResult {
10
+ const start = body.lastIndexOf(reportBlockOpen);
11
+
12
+ if (start === -1) {
13
+ return { ok: false, reason: 'The PR body holds no wringer report block.' };
14
+ }
15
+
16
+ const end = body.indexOf(reportBlockClose, start + reportBlockOpen.length);
17
+
18
+ if (end === -1) {
19
+ return { ok: false, reason: 'The wringer report block in the PR body is not closed.' };
20
+ }
21
+
22
+ const text = body.slice(start + reportBlockOpen.length, end).trim();
23
+ let parsed: unknown;
24
+
25
+ try {
26
+ parsed = JSON.parse(text);
27
+ } catch {
28
+ return { ok: false, reason: 'The wringer report block does not hold valid JSON.' };
29
+ }
30
+
31
+ const report = reportSchema.safeParse(parsed);
32
+
33
+ return report.success
34
+ ? { ok: true, report: report.data }
35
+ : {
36
+ ok: false,
37
+ reason: `The wringer report block does not match the report schema: ${report.error.message}`,
38
+ };
39
+ }
@@ -0,0 +1,34 @@
1
+ import { runEventSchema } from './run-events.ts';
2
+ import type { RunEvent } from './run-events.ts';
3
+ import { WringerError } from './wringer-error.ts';
4
+
5
+ // An event counts once its line ends: the next append cuts a last line with no newline, even one
6
+ // that parses, so a reader skips it too. A whole line that is no event is corruption.
7
+ export function parseRunLog(text: string): readonly RunEvent[] {
8
+ const lines = text
9
+ .slice(0, text.lastIndexOf('\n') + 1)
10
+ .split('\n')
11
+ .slice(0, -1);
12
+
13
+ return lines.map((line, index) => {
14
+ const parsed = runEventSchema.safeParse(parseJSONLine(line));
15
+
16
+ if (!parsed.success) {
17
+ throw new WringerError('environment', {
18
+ message: `The run log is corrupt: line ${index + 1} is not a run event.`,
19
+ });
20
+ }
21
+
22
+ return parsed.data;
23
+ });
24
+ }
25
+
26
+ function parseJSONLine(line: string): unknown {
27
+ try {
28
+ const parsed: unknown = JSON.parse(line);
29
+
30
+ return parsed;
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ }
@@ -0,0 +1,32 @@
1
+ import type { Access } from './host.ts';
2
+ import type { Effect } from './types.ts';
3
+ import { WringerError } from './wringer-error.ts';
4
+
5
+ export interface AccessInput {
6
+ readonly path: readonly string[];
7
+ readonly allowedTargets: readonly string[] | undefined;
8
+ readonly targetName: string;
9
+ readonly access: Access;
10
+ readonly effect: Effect;
11
+ readonly hint: string;
12
+ }
13
+
14
+ // A repo command may name the only targets it runs on, and a read-only target takes only reads.
15
+ export function pickAccessRefusal(input: AccessInput): WringerError | null {
16
+ const commandText = `\`${input.path.join(' ')}\``;
17
+
18
+ if (input.allowedTargets !== undefined && !input.allowedTargets.includes(input.targetName)) {
19
+ return new WringerError('target-not-allowed', {
20
+ message: `${commandText} runs only on ${input.allowedTargets.join(', ')}.`,
21
+ });
22
+ }
23
+
24
+ if (input.access === 'read-only' && input.effect !== 'read') {
25
+ return new WringerError('read-only', {
26
+ message: `${commandText} is a write, and target ${input.targetName} is read-only.`,
27
+ hint: input.hint,
28
+ });
29
+ }
30
+
31
+ return null;
32
+ }
@@ -0,0 +1,29 @@
1
+ import type { DoctorState } from './build-run-view.ts';
2
+ import type { Mode } from './run-events.ts';
3
+
4
+ export interface CommandNeedsFacts {
5
+ readonly isBuiltin: boolean;
6
+ readonly needsRun: boolean | undefined;
7
+ readonly needsDoctor: boolean | undefined;
8
+ readonly mode: Mode;
9
+ readonly doctor: DoctorState | undefined;
10
+ }
11
+
12
+ export interface CommandNeeds {
13
+ readonly needsRun: boolean;
14
+ readonly needsDoctor: boolean;
15
+ readonly readsFingerprint: boolean;
16
+ }
17
+
18
+ // A command that declares nothing needs a run and a doctor pass unless it is a built-in. A launched
19
+ // app runs this worktree's code, so only a doctor pass on a launched run needs the fingerprint.
20
+ export function pickCommandNeeds(facts: CommandNeedsFacts): CommandNeeds {
21
+ const needsRun = facts.needsRun ?? !facts.isBuiltin;
22
+ const needsDoctor = facts.needsDoctor ?? !facts.isBuiltin;
23
+
24
+ return {
25
+ needsRun,
26
+ needsDoctor,
27
+ readsFingerprint: needsDoctor && facts.mode === 'launch' && facts.doctor === 'passed',
28
+ };
29
+ }
@@ -0,0 +1,51 @@
1
+ import type { RunView } from './build-run-view.ts';
2
+ import { decideGuard } from './decide-guard.ts';
3
+ import { pickDriveKind } from './pick-drive-kind.ts';
4
+ import type { DriveKind } from './pick-drive-kind.ts';
5
+ import type { Mode } from './run-events.ts';
6
+ import { WringerError } from './wringer-error.ts';
7
+
8
+ export interface CommandFacts {
9
+ readonly needsRun: boolean;
10
+ readonly needsDoctor: boolean;
11
+ readonly isRepoCommand: boolean;
12
+ readonly run: RunView | undefined;
13
+ readonly targetName: string;
14
+ readonly mode: Mode;
15
+ readonly fingerprintChanged: boolean;
16
+ readonly deadlinePassed: boolean;
17
+ }
18
+
19
+ export type CommandPlan =
20
+ | { readonly kind: 'refuse'; readonly failure: WringerError }
21
+ | {
22
+ readonly kind: 'run';
23
+ readonly driveKind: DriveKind;
24
+
25
+ // The command drives the app, so its envelope goes into the run's log as a drive.
26
+ readonly drive: boolean;
27
+ };
28
+
29
+ // The envelope answers at the deadline without waiting for the command, so a body that would start
30
+ // after it never starts.
31
+ export function pickCommandPlan(facts: CommandFacts): CommandPlan {
32
+ const refusal = decideGuard(facts);
33
+
34
+ if (refusal !== null) {
35
+ return { kind: 'refuse', failure: refusal };
36
+ }
37
+
38
+ if (facts.deadlinePassed) {
39
+ return { kind: 'refuse', failure: new WringerError('timeout') };
40
+ }
41
+
42
+ return {
43
+ kind: 'run',
44
+ driveKind: pickDriveKind({
45
+ runStatus: facts.run?.status ?? null,
46
+ needsRun: facts.needsRun,
47
+ isRepoCommand: facts.isRepoCommand,
48
+ }),
49
+ drive: facts.needsRun && facts.needsDoctor,
50
+ };
51
+ }
@@ -0,0 +1,22 @@
1
+ export interface ContextTargetInput {
2
+ readonly targetNames: readonly string[];
3
+ readonly sessionTarget: string | undefined;
4
+ readonly savedTarget: string | undefined;
5
+ readonly commandKey: string;
6
+ }
7
+
8
+ // The target a command runs against: the one the session names, else the saved one, else the first.
9
+ // A saved target the registry no longer has stays an error for every command but `use`, which skips
10
+ // it, so `use --target` can always choose again.
11
+ export function pickContextTarget(input: ContextTargetInput): string {
12
+ const [firstTarget = ''] = input.targetNames;
13
+
14
+ const savedTarget =
15
+ input.commandKey === 'use' &&
16
+ input.savedTarget !== undefined &&
17
+ !input.targetNames.includes(input.savedTarget)
18
+ ? undefined
19
+ : input.savedTarget;
20
+
21
+ return input.sessionTarget ?? savedTarget ?? firstTarget;
22
+ }
@@ -0,0 +1,23 @@
1
+ import type { RunStatus } from './append-run-event.ts';
2
+
3
+ export interface DriveFacts {
4
+ readonly runStatus: RunStatus | null;
5
+ readonly needsRun: boolean;
6
+ readonly isRepoCommand: boolean;
7
+ }
8
+
9
+ export type DriveKind = 'run' | 'runless' | 'none';
10
+
11
+ // A command that needs a run drives it, and so does a repo command that needs none while a run is
12
+ // running, so it reaches the run's services. Any other repo command gets a drive with no run; a
13
+ // built-in or driver command that needs no run reaches no drive.
14
+ export function pickDriveKind(facts: DriveFacts): DriveKind {
15
+ if (
16
+ facts.runStatus !== null &&
17
+ (facts.needsRun || (facts.isRepoCommand && facts.runStatus === 'running'))
18
+ ) {
19
+ return 'run';
20
+ }
21
+
22
+ return !facts.needsRun && facts.isRepoCommand ? 'runless' : 'none';
23
+ }
@@ -0,0 +1,10 @@
1
+ // A command that ran on a run, such as a drive or the doctor, reports the run and feature it ran
2
+ // under, even when another command moved the pointers meanwhile. Any other command reports the
3
+ // state it left, such as the run `up` started.
4
+ export function pickEnvelopeContext<Context>(
5
+ before: Context | null,
6
+ after: Context | null,
7
+ ranOnRun: boolean,
8
+ ): Context | null {
9
+ return ranOnRun ? before : after;
10
+ }
@@ -0,0 +1,28 @@
1
+ import { WringerError } from './wringer-error.ts';
2
+
3
+ export interface PayloadInput {
4
+ readonly data: unknown;
5
+ readonly commandFailure: WringerError | null;
6
+ readonly cursors: Readonly<Record<string, number>> | WringerError;
7
+ }
8
+
9
+ export interface EnvelopePayload {
10
+ readonly data: unknown;
11
+ readonly failure: WringerError | null;
12
+ readonly cursors: Readonly<Record<string, number>>;
13
+ }
14
+
15
+ // The command's own failure leads, and a cursor read that failed after it is the envelope's
16
+ // failure. A failed command's envelope carries the data its error gives, such as the doctor's
17
+ // results; an internal error's data is its stack, which goes to the run's log instead.
18
+ export function pickEnvelopePayload(input: PayloadInput): EnvelopePayload {
19
+ const cursorsFailure = input.cursors instanceof WringerError ? input.cursors : null;
20
+ const cursors = input.cursors instanceof WringerError ? {} : input.cursors;
21
+ const failure = input.commandFailure ?? cursorsFailure;
22
+
23
+ if (failure === null) {
24
+ return { data: input.data, failure, cursors };
25
+ }
26
+
27
+ return { data: failure.code === 'internal' ? null : failure.data, failure, cursors };
28
+ }
@@ -0,0 +1,25 @@
1
+ import type { RunView } from './build-run-view.ts';
2
+
3
+ export interface EnvelopeRunContext {
4
+ readonly targetName: string;
5
+ readonly run: RunView | undefined;
6
+ }
7
+
8
+ // A lifecycle command's envelope belongs to the run it bound under the target's lock. `down` clears
9
+ // the pointer to the run it ended, so its envelope names that run from before the command, and a
10
+ // command that moved to another target names only that target's run.
11
+ export function pickEnvelopeRun(
12
+ before: EnvelopeRunContext | null,
13
+ settled: EnvelopeRunContext | null,
14
+ bound: RunView | null,
15
+ ): RunView | undefined {
16
+ if (bound !== null) {
17
+ return bound;
18
+ }
19
+
20
+ if (settled?.targetName !== before?.targetName) {
21
+ return settled?.run;
22
+ }
23
+
24
+ return settled?.run ?? before?.run;
25
+ }
@@ -0,0 +1,10 @@
1
+ import type { RunView } from './build-run-view.ts';
2
+
3
+ // A lifecycle command that bound its envelope to a run reports that run's target, even when another
4
+ // command moved the target pointer meanwhile; any other command reports its settled context's.
5
+ export function pickEnvelopeTarget(
6
+ settled: { readonly targetName: string } | null,
7
+ bound: RunView | null,
8
+ ): string | null {
9
+ return bound?.target ?? settled?.targetName ?? null;
10
+ }
@@ -0,0 +1,44 @@
1
+ import type { LateFailureEvent, RunID } from './run-events.ts';
2
+ import { toWringerError } from './to-wringer-error.ts';
3
+ import type { WringerError } from './wringer-error.ts';
4
+
5
+ export interface LateFailureFacts {
6
+ readonly runID: RunID | null;
7
+ readonly command: string;
8
+ readonly answered: WringerError | null;
9
+ readonly at: Date;
10
+ }
11
+
12
+ export interface LateFailureWrite {
13
+ // With no run, the state folder's late-failure log takes the event.
14
+ readonly runID: RunID | null;
15
+ readonly event: LateFailureEvent;
16
+ }
17
+
18
+ // Null when the work succeeded, failed with the error the envelope already answered with, or ran
19
+ // into the same deadline the envelope answered as a timeout, such as a body refused at its start.
20
+ export function pickLateFailureWrite(
21
+ settled: PromiseSettledResult<unknown>,
22
+ facts: LateFailureFacts,
23
+ ): LateFailureWrite | null {
24
+ if (settled.status === 'fulfilled' || settled.reason === facts.answered) {
25
+ return null;
26
+ }
27
+
28
+ const failure = toWringerError(settled.reason);
29
+
30
+ if (failure.code === 'timeout' && facts.answered?.code === 'timeout') {
31
+ return null;
32
+ }
33
+
34
+ return {
35
+ runID: facts.runID,
36
+ event: {
37
+ type: 'late-failure',
38
+ at: facts.at.toISOString(),
39
+ command: facts.command,
40
+ code: failure.code,
41
+ message: failure.message,
42
+ },
43
+ };
44
+ }
@@ -0,0 +1,18 @@
1
+ import type { RunView } from './build-run-view.ts';
2
+ import type { RunViewRefusal } from './create-file-state.ts';
3
+ import type { Envelope } from './envelope.ts';
4
+ import { findVerdictRefusal } from './find-verdict-refusal.ts';
5
+ import { doneCommand } from './read-verdicts.ts';
6
+
7
+ // Only a verdict that `done` gave needs the run, as it stands under the lock, to still match the
8
+ // run `done` read.
9
+ export function pickLogRefusal(
10
+ envelope: Envelope,
11
+ read: RunView | undefined,
12
+ ): RunViewRefusal | null {
13
+ if (envelope.command !== doneCommand || !envelope.ok) {
14
+ return null;
15
+ }
16
+
17
+ return (current) => findVerdictRefusal(read, current);
18
+ }
@@ -0,0 +1,15 @@
1
+ import { z } from 'zod';
2
+ import { WringerError } from './wringer-error.ts';
3
+
4
+ const gitFailureSchema = z.object({ exitCode: z.number(), stderr: z.string() });
5
+
6
+ // `merge-base` exits 1 with nothing on stderr when the two share no commit, and also exits 1 when
7
+ // it cannot read an ancestor, with the read error on stderr. Only the first means no shared commit.
8
+ export function pickMergeBaseFailure(failure: unknown, noSharedCommit: WringerError): unknown {
9
+ const data = failure instanceof WringerError ? failure.data : undefined;
10
+ const parsed = gitFailureSchema.safeParse(data);
11
+
12
+ return parsed.success && parsed.data.exitCode === 1 && parsed.data.stderr.trim() === ''
13
+ ? noSharedCommit
14
+ : failure;
15
+ }