@bluewombat/runtime 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.
- package/README.md +236 -142
- package/SPECS.md +138 -97
- package/dist/cli.js +35 -37
- package/dist/cli.js.map +1 -1
- package/dist/config/agent-shorthand.d.ts +24 -4
- package/dist/config/agent-shorthand.d.ts.map +1 -1
- package/dist/config/agent-shorthand.js +65 -28
- package/dist/config/agent-shorthand.js.map +1 -1
- package/dist/config/defaults.d.ts +0 -2
- package/dist/config/defaults.d.ts.map +1 -1
- package/dist/config/defaults.js +0 -2
- package/dist/config/defaults.js.map +1 -1
- package/dist/config/find-config.d.ts +3 -2
- package/dist/config/find-config.d.ts.map +1 -1
- package/dist/config/find-config.js +4 -4
- package/dist/config/find-config.js.map +1 -1
- package/dist/config/home.d.ts +2 -0
- package/dist/config/home.d.ts.map +1 -1
- package/dist/config/home.js +4 -2
- package/dist/config/home.js.map +1 -1
- package/dist/config/load-config.d.ts +2 -2
- package/dist/config/load-config.d.ts.map +1 -1
- package/dist/config/load-config.js +342 -73
- package/dist/config/load-config.js.map +1 -1
- package/dist/config/parse-args.d.ts +1 -1
- package/dist/config/parse-args.d.ts.map +1 -1
- package/dist/config/parse-args.js +132 -103
- package/dist/config/parse-args.js.map +1 -1
- package/dist/config/types.d.ts +142 -38
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js +13 -1
- package/dist/config/types.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loop/authority.d.ts +41 -5
- package/dist/loop/authority.d.ts.map +1 -1
- package/dist/loop/authority.js +68 -24
- package/dist/loop/authority.js.map +1 -1
- package/dist/loop/context.d.ts +23 -3
- package/dist/loop/context.d.ts.map +1 -1
- package/dist/loop/deliveries.d.ts +16 -16
- package/dist/loop/deliveries.d.ts.map +1 -1
- package/dist/loop/deliveries.js +35 -187
- package/dist/loop/deliveries.js.map +1 -1
- package/dist/loop/describe.d.ts +4 -6
- package/dist/loop/describe.d.ts.map +1 -1
- package/dist/loop/describe.js +2 -2
- package/dist/loop/describe.js.map +1 -1
- package/dist/loop/drive.d.ts +11 -7
- package/dist/loop/drive.d.ts.map +1 -1
- package/dist/loop/drive.js +24 -38
- package/dist/loop/drive.js.map +1 -1
- package/dist/loop/features.d.ts +23 -0
- package/dist/loop/features.d.ts.map +1 -0
- package/dist/loop/features.js +834 -0
- package/dist/loop/features.js.map +1 -0
- package/dist/loop/journal.d.ts +1 -1
- package/dist/loop/journal.js +2 -2
- package/dist/loop/journal.js.map +1 -1
- package/dist/loop/lock.d.ts +1 -1
- package/dist/loop/lock.js +1 -1
- package/dist/loop/open-host.d.ts.map +1 -1
- package/dist/loop/open-host.js +132 -31
- package/dist/loop/open-host.js.map +1 -1
- package/dist/loop/probe.d.ts +1 -1
- package/dist/loop/probe.d.ts.map +1 -1
- package/dist/loop/probe.js +51 -22
- package/dist/loop/probe.js.map +1 -1
- package/dist/loop/process-tree.d.ts +1 -1
- package/dist/loop/process-tree.js +1 -1
- package/dist/loop/report.d.ts +9 -7
- package/dist/loop/report.d.ts.map +1 -1
- package/dist/loop/report.js +49 -106
- package/dist/loop/report.js.map +1 -1
- package/dist/loop/streams.d.ts +5 -5
- package/dist/loop/streams.d.ts.map +1 -1
- package/dist/loop/streams.js +3 -3
- package/dist/loop/streams.js.map +1 -1
- package/dist/loop/tick.d.ts +6 -4
- package/dist/loop/tick.d.ts.map +1 -1
- package/dist/loop/tick.js +50 -78
- package/dist/loop/tick.js.map +1 -1
- package/dist/loop/trace.d.ts +5 -5
- package/dist/loop/trace.d.ts.map +1 -1
- package/dist/loop/trace.js +5 -14
- package/dist/loop/trace.js.map +1 -1
- package/dist/loop/transformers.d.ts +18 -14
- package/dist/loop/transformers.d.ts.map +1 -1
- package/dist/loop/transformers.js +139 -74
- package/dist/loop/transformers.js.map +1 -1
- package/dist/loop/warm.d.ts +3 -3
- package/dist/loop/warm.js +4 -4
- package/dist/loop/warm.js.map +1 -1
- package/dist/loop/work-line.d.ts +1 -1
- package/dist/operator/agent-plugins.d.ts +11 -0
- package/dist/operator/agent-plugins.d.ts.map +1 -1
- package/dist/operator/agent-plugins.js +17 -11
- package/dist/operator/agent-plugins.js.map +1 -1
- package/dist/operator/board.d.ts +3 -8
- package/dist/operator/board.d.ts.map +1 -1
- package/dist/operator/board.js +2 -11
- package/dist/operator/board.js.map +1 -1
- package/dist/operator/cancel.d.ts +2 -2
- package/dist/operator/cancel.js +6 -6
- package/dist/operator/cancel.js.map +1 -1
- package/dist/operator/doctor.d.ts +9 -0
- package/dist/operator/doctor.d.ts.map +1 -1
- package/dist/operator/doctor.js +110 -53
- package/dist/operator/doctor.js.map +1 -1
- package/dist/operator/explain.d.ts +76 -0
- package/dist/operator/explain.d.ts.map +1 -0
- package/dist/operator/explain.js +585 -0
- package/dist/operator/explain.js.map +1 -0
- package/dist/operator/film.d.ts +3 -11
- package/dist/operator/film.d.ts.map +1 -1
- package/dist/operator/film.js +8 -93
- package/dist/operator/film.js.map +1 -1
- package/dist/operator/init.d.ts.map +1 -1
- package/dist/operator/init.js +52 -55
- package/dist/operator/init.js.map +1 -1
- package/dist/operator/live.js +1 -1
- package/dist/operator/live.js.map +1 -1
- package/dist/operator/log.js +12 -6
- package/dist/operator/log.js.map +1 -1
- package/dist/operator/release.d.ts +2 -2
- package/dist/operator/release.js +7 -7
- package/dist/operator/release.js.map +1 -1
- package/dist/operator/run.d.ts +25 -0
- package/dist/operator/run.d.ts.map +1 -0
- package/dist/operator/run.js +70 -0
- package/dist/operator/run.js.map +1 -0
- package/dist/operator/setup.d.ts.map +1 -1
- package/dist/operator/setup.js +22 -16
- package/dist/operator/setup.js.map +1 -1
- package/dist/plugins/manager.d.ts +33 -2
- package/dist/plugins/manager.d.ts.map +1 -1
- package/dist/plugins/manager.js +67 -0
- package/dist/plugins/manager.js.map +1 -1
- package/package.json +15 -16
|
@@ -2,29 +2,44 @@ import { existsSync, readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { dirname, resolve } from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
4
|
import { expandAgentPass, readAgents, SHORT_PASS_KEYS, } from "./agent-shorthand.js";
|
|
5
|
+
import { SKIP_WHEN } from "./types.js";
|
|
5
6
|
const KNOWN_KEYS = new Set([
|
|
6
|
-
"
|
|
7
|
-
"
|
|
7
|
+
"featureTracker",
|
|
8
|
+
"ticketTracker",
|
|
8
9
|
"home",
|
|
9
10
|
"workLine",
|
|
10
11
|
"workspaceRoot",
|
|
11
12
|
"ledger",
|
|
12
13
|
"persist",
|
|
13
14
|
"agents",
|
|
14
|
-
"planner",
|
|
15
15
|
"builder",
|
|
16
|
-
"
|
|
16
|
+
"ticket",
|
|
17
|
+
"feature",
|
|
17
18
|
"authority",
|
|
18
19
|
"observability",
|
|
19
20
|
"timeoutMs",
|
|
20
|
-
"maxRefusals",
|
|
21
21
|
"pollIntervalMs",
|
|
22
22
|
]);
|
|
23
|
-
const WORK_LINE_KEYS = new Set([
|
|
23
|
+
const WORK_LINE_KEYS = new Set([
|
|
24
|
+
"stable",
|
|
25
|
+
"targetStable",
|
|
26
|
+
"branch",
|
|
27
|
+
"isolation",
|
|
28
|
+
"isolationOptions",
|
|
29
|
+
"warm",
|
|
30
|
+
]);
|
|
24
31
|
const PASS_KEYS = new Set(["cmd", "timeoutMs"]);
|
|
25
32
|
const GATED_PASS_KEYS = new Set(["cmd", "timeoutMs", "gates"]);
|
|
26
|
-
const STAGE_KEYS = new Set(["producer", "repair", "maxAttempts"]);
|
|
27
|
-
const
|
|
33
|
+
const STAGE_KEYS = new Set(["tests", "e2e", "producer", "repair", "gates", "maxAttempts"]);
|
|
34
|
+
const TICKET_KEYS = new Set([
|
|
35
|
+
"maxParallel",
|
|
36
|
+
"maxAttempts",
|
|
37
|
+
"maxRefusals",
|
|
38
|
+
"fix",
|
|
39
|
+
"validate",
|
|
40
|
+
"gates",
|
|
41
|
+
]);
|
|
42
|
+
const FEATURE_KEYS = new Set(["maxAttempts", "maxRefusals", "fix", "validate", "gates"]);
|
|
28
43
|
const GATES_KEYS = new Set(["defaultTimeoutMs", "gates"]);
|
|
29
44
|
const AUTHORITY_KEYS = new Set(["enabled", "publish", "refresh", "describe"]);
|
|
30
45
|
const OBSERVABILITY_KEYS = new Set(["streams"]);
|
|
@@ -62,19 +77,14 @@ export function loadConfig(path) {
|
|
|
62
77
|
}
|
|
63
78
|
const configDir = dirname(file);
|
|
64
79
|
const invocation = { configDir };
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
if (object.managerOptions !== undefined) {
|
|
73
|
-
const options = object.managerOptions;
|
|
74
|
-
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
75
|
-
return { ok: false, reason: 'Config "managerOptions" must be a YAML mapping.' };
|
|
80
|
+
for (const side of ["featureTracker", "ticketTracker"]) {
|
|
81
|
+
const spec = readManagerSpec(object[side], side);
|
|
82
|
+
if (spec.ok === false) {
|
|
83
|
+
return spec;
|
|
84
|
+
}
|
|
85
|
+
if (spec.value !== undefined) {
|
|
86
|
+
invocation[side] = spec.value;
|
|
76
87
|
}
|
|
77
|
-
invocation.managerOptions = { ...options };
|
|
78
88
|
}
|
|
79
89
|
const home = stringField(object, "home");
|
|
80
90
|
if (home.ok === false) {
|
|
@@ -91,6 +101,9 @@ export function loadConfig(path) {
|
|
|
91
101
|
if (workLine.stable !== undefined) {
|
|
92
102
|
invocation.workLineStable = workLine.stable;
|
|
93
103
|
}
|
|
104
|
+
if (workLine.targetStable !== undefined) {
|
|
105
|
+
invocation.workLineTargetStable = workLine.targetStable;
|
|
106
|
+
}
|
|
94
107
|
if (workLine.branch !== undefined) {
|
|
95
108
|
invocation.workLineBranch = workLine.branch;
|
|
96
109
|
}
|
|
@@ -133,13 +146,6 @@ export function loadConfig(path) {
|
|
|
133
146
|
}
|
|
134
147
|
agents = read.value;
|
|
135
148
|
}
|
|
136
|
-
if (object.planner !== undefined) {
|
|
137
|
-
const planner = readGatedPass(object.planner, configDir, "planner", agents);
|
|
138
|
-
if (planner.ok === false) {
|
|
139
|
-
return planner;
|
|
140
|
-
}
|
|
141
|
-
invocation.planner = planner.value;
|
|
142
|
-
}
|
|
143
149
|
if (object.builder !== undefined) {
|
|
144
150
|
const builder = readBuilder(object.builder, configDir, agents);
|
|
145
151
|
if (builder.ok === false) {
|
|
@@ -147,12 +153,19 @@ export function loadConfig(path) {
|
|
|
147
153
|
}
|
|
148
154
|
invocation.builder = builder.value;
|
|
149
155
|
}
|
|
150
|
-
if (object.
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
153
|
-
return
|
|
156
|
+
if (object.ticket !== undefined) {
|
|
157
|
+
const ticket = readTicket(object.ticket, configDir, agents);
|
|
158
|
+
if (ticket.ok === false) {
|
|
159
|
+
return ticket;
|
|
160
|
+
}
|
|
161
|
+
invocation.ticket = ticket.value;
|
|
162
|
+
}
|
|
163
|
+
if (object.feature !== undefined) {
|
|
164
|
+
const feature = readFeature(object.feature, configDir, agents);
|
|
165
|
+
if (feature.ok === false) {
|
|
166
|
+
return feature;
|
|
154
167
|
}
|
|
155
|
-
invocation.
|
|
168
|
+
invocation.feature = feature.value;
|
|
156
169
|
}
|
|
157
170
|
if (object.authority !== undefined) {
|
|
158
171
|
const authority = readAuthority(object.authority, configDir, agents);
|
|
@@ -175,15 +188,6 @@ export function loadConfig(path) {
|
|
|
175
188
|
if (timeoutMs.value !== undefined) {
|
|
176
189
|
invocation.timeoutMs = timeoutMs.value;
|
|
177
190
|
}
|
|
178
|
-
for (const key of ["maxRefusals"]) {
|
|
179
|
-
const read = positiveIntField(object, key);
|
|
180
|
-
if (read.ok === false) {
|
|
181
|
-
return read;
|
|
182
|
-
}
|
|
183
|
-
if (read.value !== undefined) {
|
|
184
|
-
invocation[key] = read.value;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
191
|
const pollIntervalMs = positiveIntField(object, "pollIntervalMs");
|
|
188
192
|
if (pollIntervalMs.ok === false) {
|
|
189
193
|
return pollIntervalMs;
|
|
@@ -191,8 +195,79 @@ export function loadConfig(path) {
|
|
|
191
195
|
if (pollIntervalMs.value !== undefined) {
|
|
192
196
|
invocation.pollIntervalMs = pollIntervalMs.value;
|
|
193
197
|
}
|
|
198
|
+
const e2eRefusal = checkE2eDeclaration(invocation);
|
|
199
|
+
if (e2eRefusal !== undefined) {
|
|
200
|
+
return { ok: false, reason: e2eRefusal };
|
|
201
|
+
}
|
|
194
202
|
return { ok: true, invocation };
|
|
195
203
|
}
|
|
204
|
+
/**
|
|
205
|
+
* The e2e check is declared by a pair: `builder.e2e`, which makes the Ticket that holds the
|
|
206
|
+
* suite, and a Feature Gate with `skipWhen: skip-test-e2e`, which runs it. One without the other
|
|
207
|
+
* is a check that never runs or a Ticket nobody judges, so neither is accepted alone. A `skipWhen`
|
|
208
|
+
* outside a Feature's Gates has nothing to read.
|
|
209
|
+
*/
|
|
210
|
+
function checkE2eDeclaration(invocation) {
|
|
211
|
+
const outside = [
|
|
212
|
+
["builder.gates", invocation.builder?.producer.gates],
|
|
213
|
+
["builder.tests.gates", invocation.builder?.tests?.gates],
|
|
214
|
+
["builder.e2e.gates", invocation.builder?.e2e?.gates],
|
|
215
|
+
["ticket.gates", invocation.ticket?.gates],
|
|
216
|
+
["ticket.fix.gates", invocation.ticket?.fix?.gates],
|
|
217
|
+
["ticket.validate.gates", invocation.ticket?.validate?.gates],
|
|
218
|
+
];
|
|
219
|
+
for (const [key, gates] of outside) {
|
|
220
|
+
const bad = gates?.find((gate) => gate.skipWhen !== undefined);
|
|
221
|
+
if (bad !== undefined) {
|
|
222
|
+
return `Config "${key}" gate "${bad.id}" has "skipWhen", which only a Feature's gates (feature.validate, feature.gates, feature.fix) can have.`;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const feature = invocation.feature;
|
|
226
|
+
const skippable = [
|
|
227
|
+
...(feature?.validate.gates ?? []),
|
|
228
|
+
...(feature?.gates ?? []),
|
|
229
|
+
...(feature?.fix.gates ?? []),
|
|
230
|
+
].filter((gate) => gate.skipWhen === "skip-test-e2e");
|
|
231
|
+
const declared = invocation.builder?.e2e !== undefined;
|
|
232
|
+
if (skippable.length > 0 && !declared) {
|
|
233
|
+
return `Config gate "${skippable[0]?.id}" has "skipWhen: skip-test-e2e", so the e2e check is declared: "builder.e2e" is required (the pass that makes the Ticket holding the suite).`;
|
|
234
|
+
}
|
|
235
|
+
if (declared && feature !== undefined && skippable.length === 0) {
|
|
236
|
+
return 'Config "builder.e2e" is declared, but no Feature gate runs the suite. Add a gate to "feature.validate.gates" with "skipWhen: skip-test-e2e".';
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
const MANAGER_SPEC_KEYS = new Set(["package", "options"]);
|
|
241
|
+
function readManagerSpec(value, side) {
|
|
242
|
+
if (value === undefined) {
|
|
243
|
+
return { ok: true, value: undefined };
|
|
244
|
+
}
|
|
245
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
246
|
+
return { ok: false, reason: `Config "${side}" must be a YAML mapping (package, options).` };
|
|
247
|
+
}
|
|
248
|
+
const object = value;
|
|
249
|
+
for (const key of Object.keys(object)) {
|
|
250
|
+
if (!MANAGER_SPEC_KEYS.has(key)) {
|
|
251
|
+
return { ok: false, reason: `Config "${side}" has unknown key "${key}".` };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const spec = {};
|
|
255
|
+
const name = stringField(object, "package");
|
|
256
|
+
if (name.ok === false) {
|
|
257
|
+
return { ok: false, reason: name.reason.replace('"package"', `"${side}.package"`) };
|
|
258
|
+
}
|
|
259
|
+
if (name.value !== undefined) {
|
|
260
|
+
spec.package = name.value;
|
|
261
|
+
}
|
|
262
|
+
if (object.options !== undefined) {
|
|
263
|
+
const options = object.options;
|
|
264
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
265
|
+
return { ok: false, reason: `Config "${side}.options" must be a YAML mapping.` };
|
|
266
|
+
}
|
|
267
|
+
spec.options = { ...options };
|
|
268
|
+
}
|
|
269
|
+
return { ok: true, value: spec };
|
|
270
|
+
}
|
|
196
271
|
function stringField(object, key) {
|
|
197
272
|
const value = object[key];
|
|
198
273
|
if (value === undefined) {
|
|
@@ -239,6 +314,10 @@ function readWorkLine(value, configDir) {
|
|
|
239
314
|
if (stable.ok === false) {
|
|
240
315
|
return { ok: false, reason: `Config "workLine.stable" must be a non-empty string.` };
|
|
241
316
|
}
|
|
317
|
+
const targetStable = stringField(object, "targetStable");
|
|
318
|
+
if (targetStable.ok === false) {
|
|
319
|
+
return { ok: false, reason: `Config "workLine.targetStable" must be a non-empty string.` };
|
|
320
|
+
}
|
|
242
321
|
const branch = stringField(object, "branch");
|
|
243
322
|
if (branch.ok === false) {
|
|
244
323
|
return { ok: false, reason: `Config "workLine.branch" must be a non-empty string.` };
|
|
@@ -268,6 +347,9 @@ function readWorkLine(value, configDir) {
|
|
|
268
347
|
return {
|
|
269
348
|
ok: true,
|
|
270
349
|
...(stable.value === undefined ? {} : { stable: resolve(configDir, stable.value) }),
|
|
350
|
+
...(targetStable.value === undefined
|
|
351
|
+
? {}
|
|
352
|
+
: { targetStable: resolve(configDir, targetStable.value) }),
|
|
271
353
|
...(branch.value === undefined ? {} : { branch: branch.value }),
|
|
272
354
|
...(isolation.value === undefined ? {} : { isolation: isolation.value }),
|
|
273
355
|
...(isolationOptions === undefined
|
|
@@ -410,7 +492,7 @@ function readObservability(value, configDir) {
|
|
|
410
492
|
// This system writes here, so it resolves against the config directory —
|
|
411
493
|
// the same rule as a slot's `--transcript-dir`. Left relative to whatever
|
|
412
494
|
// a child's working directory happens to be, the films would land inside a
|
|
413
|
-
// Task workspace and be folded into the
|
|
495
|
+
// Task workspace and be folded into the ticket.
|
|
414
496
|
spec.dir = resolve(configDir, fields.dir);
|
|
415
497
|
}
|
|
416
498
|
if (fields.keep !== undefined) {
|
|
@@ -439,8 +521,9 @@ function readGates(value, configDir, key) {
|
|
|
439
521
|
if (fallback.ok === false) {
|
|
440
522
|
return { ok: false, reason: `Config "${key}.defaultTimeoutMs" must be a positive integer.` };
|
|
441
523
|
}
|
|
524
|
+
// A bare `gates:` in YAML is a null: an empty list, written the short way.
|
|
442
525
|
const entries = object.gates;
|
|
443
|
-
if (entries === undefined) {
|
|
526
|
+
if (entries === undefined || entries === null) {
|
|
444
527
|
return { ok: true, value: [] };
|
|
445
528
|
}
|
|
446
529
|
if (!Array.isArray(entries)) {
|
|
@@ -478,7 +561,19 @@ function readGates(value, configDir, key) {
|
|
|
478
561
|
reason: `Config gate "${id}" has no "timeoutMs", and "${key}" names no "defaultTimeoutMs".`,
|
|
479
562
|
};
|
|
480
563
|
}
|
|
481
|
-
|
|
564
|
+
const skipWhen = entry.skipWhen;
|
|
565
|
+
if (skipWhen !== undefined && !SKIP_WHEN.includes(skipWhen)) {
|
|
566
|
+
return {
|
|
567
|
+
ok: false,
|
|
568
|
+
reason: `Config gate "${id}" has "skipWhen: ${String(skipWhen)}". Known: ${SKIP_WHEN.join(", ")}.`,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
gates.push({
|
|
572
|
+
id,
|
|
573
|
+
argv: resolveArgv(argv, configDir),
|
|
574
|
+
timeoutMs,
|
|
575
|
+
...(skipWhen === undefined ? {} : { skipWhen: skipWhen }),
|
|
576
|
+
});
|
|
482
577
|
}
|
|
483
578
|
return { ok: true, value: gates };
|
|
484
579
|
}
|
|
@@ -487,7 +582,7 @@ function readGates(value, configDir, key) {
|
|
|
487
582
|
*
|
|
488
583
|
* `timeoutMs` is required: without it nothing bounds an agent, and an agent
|
|
489
584
|
* that never returns is the failure this shape exists to prevent. `repair` and
|
|
490
|
-
* `
|
|
585
|
+
* `ticket` inherit both the command and the ceiling, so a Project with one
|
|
491
586
|
* agent writes one command.
|
|
492
587
|
*/
|
|
493
588
|
function readPass(value, configDir, key) {
|
|
@@ -520,12 +615,13 @@ function readPass(value, configDir, key) {
|
|
|
520
615
|
};
|
|
521
616
|
}
|
|
522
617
|
/**
|
|
523
|
-
* A command, and its own Gate sequence — `
|
|
524
|
-
* `builder.
|
|
525
|
-
*
|
|
526
|
-
*
|
|
618
|
+
* A command, and its own Gate sequence — `ticket.fix`, `ticket.validate`.
|
|
619
|
+
* `builder.producer` and `builder.repair` read the same shape with `gated`
|
|
620
|
+
* false: their Gates are the one `builder.gates` list, read by `readBuilder`.
|
|
621
|
+
* Deliberately its own ~10 lines rather than a parameterized `readPass`: each
|
|
622
|
+
* reader's error messages and key set stay independently legible.
|
|
527
623
|
*/
|
|
528
|
-
function readGatedPass(value, configDir, key, agents) {
|
|
624
|
+
function readGatedPass(value, configDir, key, agents, gated = true) {
|
|
529
625
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
530
626
|
return {
|
|
531
627
|
ok: false,
|
|
@@ -535,8 +631,14 @@ function readGatedPass(value, configDir, key, agents) {
|
|
|
535
631
|
const object = value;
|
|
536
632
|
const short = object.agent !== undefined;
|
|
537
633
|
for (const name of Object.keys(object)) {
|
|
538
|
-
|
|
539
|
-
|
|
634
|
+
if (!gated && name === "gates") {
|
|
635
|
+
return {
|
|
636
|
+
ok: false,
|
|
637
|
+
reason: `Config "${key}" has no "gates". The builder's Gates are "builder.gates": ` +
|
|
638
|
+
"one list, judging the producer and the repair alike.",
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
const known = GATED_PASS_KEYS.has(name) || (short && SHORT_PASS_KEYS.has(name));
|
|
540
642
|
if (!known) {
|
|
541
643
|
return { ok: false, reason: `Config "${key}" has unknown key "${name}".` };
|
|
542
644
|
}
|
|
@@ -577,7 +679,80 @@ function readGatedPass(value, configDir, key, agents) {
|
|
|
577
679
|
return { ok: true, value: { cmd, timeoutMs: timeoutMs.value, gates } };
|
|
578
680
|
}
|
|
579
681
|
/**
|
|
580
|
-
*
|
|
682
|
+
* The pass that writes the tests first: an agent and the Gates that check what
|
|
683
|
+
* it wrote, or `agent: false` and nothing else, which writes none. Gates with
|
|
684
|
+
* no agent would have nothing to check, so they are refused rather than ignored.
|
|
685
|
+
*/
|
|
686
|
+
function readTests(value, configDir, agents) {
|
|
687
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
688
|
+
return {
|
|
689
|
+
ok: false,
|
|
690
|
+
reason: 'Config "builder.tests" must be a YAML mapping with an agent (or "cmd") and "timeoutMs", or "agent: false".',
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
const object = value;
|
|
694
|
+
if (object.agent !== false) {
|
|
695
|
+
return readGatedPass(value, configDir, "builder.tests", agents, true);
|
|
696
|
+
}
|
|
697
|
+
for (const name of Object.keys(object)) {
|
|
698
|
+
if (name !== "agent" && name !== "gates") {
|
|
699
|
+
return {
|
|
700
|
+
ok: false,
|
|
701
|
+
reason: `Config "builder.tests" has "agent: false", so it has no "${name}": no tests are written first.`,
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
const emptyList = Array.isArray(object.gates) && object.gates.length === 0;
|
|
706
|
+
if (object.gates !== undefined && object.gates !== null && !emptyList) {
|
|
707
|
+
const gates = readGates(object.gates, configDir, "builder.tests.gates");
|
|
708
|
+
if (gates.ok === false) {
|
|
709
|
+
return gates;
|
|
710
|
+
}
|
|
711
|
+
if (gates.value.length > 0) {
|
|
712
|
+
return {
|
|
713
|
+
ok: false,
|
|
714
|
+
reason: 'Config "builder.tests" has "agent: false", so its gates would have no tests to check. Remove them, or give it an agent.',
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return { ok: true, value: { agent: false, gates: [] } };
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* A review: an agent and its Gates, or `agent: false` and the Gates alone.
|
|
722
|
+
* The agent is turned off by saying so; a review with neither an agent nor a
|
|
723
|
+
* Gate would judge nothing, and is refused.
|
|
724
|
+
*/
|
|
725
|
+
function readReview(value, configDir, key, agents) {
|
|
726
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
727
|
+
return {
|
|
728
|
+
ok: false,
|
|
729
|
+
reason: `Config "${key}" must be a YAML mapping with an agent (or "cmd") and "timeoutMs", or "agent: false" and "gates".`,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const object = value;
|
|
733
|
+
if (object.agent !== false) {
|
|
734
|
+
return readGatedPass(value, configDir, key, agents);
|
|
735
|
+
}
|
|
736
|
+
for (const name of Object.keys(object)) {
|
|
737
|
+
if (name !== "agent" && name !== "gates") {
|
|
738
|
+
return {
|
|
739
|
+
ok: false,
|
|
740
|
+
reason: `Config "${key}" has "agent: false", so it has no "${name}": only its "gates" run.`,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
// No Gates either: the review is off, which is how a Project says it.
|
|
745
|
+
if (object.gates === undefined || object.gates === null) {
|
|
746
|
+
return { ok: true, value: { agent: false, gates: [] } };
|
|
747
|
+
}
|
|
748
|
+
const gates = readGates(object.gates, configDir, `${key}.gates`);
|
|
749
|
+
if (gates.ok === false) {
|
|
750
|
+
return gates;
|
|
751
|
+
}
|
|
752
|
+
return { ok: true, value: { agent: false, gates: gates.value } };
|
|
753
|
+
}
|
|
754
|
+
/**
|
|
755
|
+
* Making the unit of work: both passes named, neither borrowing the other's command.
|
|
581
756
|
*/
|
|
582
757
|
function readBuilder(value, configDir, agents) {
|
|
583
758
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
@@ -589,23 +764,53 @@ function readBuilder(value, configDir, agents) {
|
|
|
589
764
|
return { ok: false, reason: `Config "builder" has unknown key "${name}".` };
|
|
590
765
|
}
|
|
591
766
|
}
|
|
767
|
+
if (object.tests === undefined) {
|
|
768
|
+
return {
|
|
769
|
+
ok: false,
|
|
770
|
+
reason: 'Config "builder" needs "tests": the agent that writes the tests before the code. To write none first, say "tests: { agent: false }".',
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
const readTestsPass = readTests(object.tests, configDir, agents);
|
|
774
|
+
if (readTestsPass.ok === false) {
|
|
775
|
+
return readTestsPass;
|
|
776
|
+
}
|
|
777
|
+
let e2e;
|
|
778
|
+
if (object.e2e !== undefined) {
|
|
779
|
+
const read = readGatedPass(object.e2e, configDir, "builder.e2e", agents);
|
|
780
|
+
if (read.ok === false) {
|
|
781
|
+
return read;
|
|
782
|
+
}
|
|
783
|
+
e2e = read.value;
|
|
784
|
+
}
|
|
592
785
|
if (object.producer === undefined) {
|
|
593
786
|
return { ok: false, reason: 'Config "builder" needs a "producer".' };
|
|
594
787
|
}
|
|
595
788
|
if (object.repair === undefined) {
|
|
596
789
|
return { ok: false, reason: 'Config "builder" needs a "repair".' };
|
|
597
790
|
}
|
|
598
|
-
const readProducer = readGatedPass(object.producer, configDir, "builder.producer", agents);
|
|
791
|
+
const readProducer = readGatedPass(object.producer, configDir, "builder.producer", agents, false);
|
|
599
792
|
if (readProducer.ok === false) {
|
|
600
793
|
return readProducer;
|
|
601
794
|
}
|
|
602
|
-
const readRepair = readGatedPass(object.repair, configDir, "builder.repair", agents);
|
|
795
|
+
const readRepair = readGatedPass(object.repair, configDir, "builder.repair", agents, false);
|
|
603
796
|
if (readRepair.ok === false) {
|
|
604
797
|
return readRepair;
|
|
605
798
|
}
|
|
799
|
+
let gates = [];
|
|
800
|
+
if (object.gates !== undefined) {
|
|
801
|
+
const read = readGates(object.gates, configDir, "builder.gates");
|
|
802
|
+
if (read.ok === false) {
|
|
803
|
+
return read;
|
|
804
|
+
}
|
|
805
|
+
gates = read.value;
|
|
806
|
+
}
|
|
807
|
+
// One list, two passes: a repair is judged by exactly what judged the producer,
|
|
808
|
+
// so it cannot undo what a Gate refused. Implementer still takes two sequences.
|
|
606
809
|
const spec = {
|
|
607
|
-
|
|
608
|
-
|
|
810
|
+
tests: readTestsPass.value,
|
|
811
|
+
producer: { ...readProducer.value, gates },
|
|
812
|
+
repair: { ...readRepair.value, gates: [...gates] },
|
|
813
|
+
...(e2e === undefined ? {} : { e2e }),
|
|
609
814
|
};
|
|
610
815
|
const maxAttempts = positiveIntField(object, "maxAttempts");
|
|
611
816
|
if (maxAttempts.ok === false) {
|
|
@@ -617,50 +822,58 @@ function readBuilder(value, configDir, agents) {
|
|
|
617
822
|
return { ok: true, value: spec };
|
|
618
823
|
}
|
|
619
824
|
/**
|
|
620
|
-
* Judging the assembled
|
|
825
|
+
* Judging the assembled ticket. A command here is a fix of what a judgement
|
|
621
826
|
* refused, not a second first-pass of the request.
|
|
622
827
|
*/
|
|
623
|
-
function
|
|
828
|
+
function readTicket(value, configDir, agents) {
|
|
624
829
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
625
|
-
return { ok: false, reason: 'Config "
|
|
830
|
+
return { ok: false, reason: 'Config "ticket" must be a YAML mapping.' };
|
|
626
831
|
}
|
|
627
832
|
const object = value;
|
|
628
833
|
for (const name of Object.keys(object)) {
|
|
629
834
|
if (name === "producer" || name === "repair") {
|
|
630
835
|
return {
|
|
631
836
|
ok: false,
|
|
632
|
-
reason: `Config "
|
|
633
|
-
'("
|
|
837
|
+
reason: `Config "ticket" has no "${name}". The assembled ticket is judged ` +
|
|
838
|
+
'("ticket.gates"); a refusal is "ticket.fix".',
|
|
634
839
|
};
|
|
635
840
|
}
|
|
636
|
-
if (!
|
|
637
|
-
return { ok: false, reason: `Config "
|
|
841
|
+
if (!TICKET_KEYS.has(name)) {
|
|
842
|
+
return { ok: false, reason: `Config "ticket" has unknown key "${name}".` };
|
|
638
843
|
}
|
|
639
844
|
}
|
|
845
|
+
if (object.validate === undefined) {
|
|
846
|
+
return {
|
|
847
|
+
ok: false,
|
|
848
|
+
reason: 'Config "ticket" needs a "validate": the review of the assembled ticket. To turn it off, write "validate: { agent: false }".',
|
|
849
|
+
};
|
|
850
|
+
}
|
|
640
851
|
const spec = { gates: [] };
|
|
641
852
|
if (object.fix !== undefined) {
|
|
642
|
-
const read = readGatedPass(object.fix, configDir, "
|
|
853
|
+
const read = readGatedPass(object.fix, configDir, "ticket.fix", agents);
|
|
643
854
|
if (read.ok === false) {
|
|
644
855
|
return read;
|
|
645
856
|
}
|
|
646
857
|
spec.fix = read.value;
|
|
647
858
|
}
|
|
648
859
|
if (object.validate !== undefined) {
|
|
649
|
-
const read =
|
|
860
|
+
const read = readReview(object.validate, configDir, "ticket.validate", agents);
|
|
650
861
|
if (read.ok === false) {
|
|
651
862
|
return read;
|
|
652
863
|
}
|
|
653
864
|
spec.validate = read.value;
|
|
654
865
|
}
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
866
|
+
for (const key of ["maxParallel", "maxAttempts", "maxRefusals"]) {
|
|
867
|
+
const read = positiveIntField(object, key);
|
|
868
|
+
if (read.ok === false) {
|
|
869
|
+
return { ok: false, reason: `Config "ticket.${key}" must be a positive integer.` };
|
|
870
|
+
}
|
|
871
|
+
if (read.value !== undefined) {
|
|
872
|
+
spec[key] = read.value;
|
|
873
|
+
}
|
|
661
874
|
}
|
|
662
875
|
if (object.gates !== undefined) {
|
|
663
|
-
const gates = readGates(object.gates, configDir, "
|
|
876
|
+
const gates = readGates(object.gates, configDir, "ticket.gates");
|
|
664
877
|
if (gates.ok === false) {
|
|
665
878
|
return gates;
|
|
666
879
|
}
|
|
@@ -668,6 +881,62 @@ function readAssembly(value, configDir, agents) {
|
|
|
668
881
|
}
|
|
669
882
|
return { ok: true, value: spec };
|
|
670
883
|
}
|
|
884
|
+
/**
|
|
885
|
+
* Judging a Feature's whole line, and repairing what that judgement refused.
|
|
886
|
+
* Its own gates, fix and budget: nothing is shared with a Ticket's.
|
|
887
|
+
*/
|
|
888
|
+
function readFeature(value, configDir, agents) {
|
|
889
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
890
|
+
return { ok: false, reason: 'Config "feature" must be a YAML mapping.' };
|
|
891
|
+
}
|
|
892
|
+
const object = value;
|
|
893
|
+
for (const name of Object.keys(object)) {
|
|
894
|
+
if (!FEATURE_KEYS.has(name)) {
|
|
895
|
+
return { ok: false, reason: `Config "feature" has unknown key "${name}".` };
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
if (object.fix === undefined) {
|
|
899
|
+
return {
|
|
900
|
+
ok: false,
|
|
901
|
+
reason: 'Config "feature" needs a "fix": what repairs the line when its gates refuse it. There is no feature without one.',
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
if (object.validate === undefined) {
|
|
905
|
+
return {
|
|
906
|
+
ok: false,
|
|
907
|
+
reason: 'Config "feature" needs a "validate": the review of the whole line against the Feature\'s request, whose gates run the project\'s own checks on it.',
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
if (object.gates === undefined) {
|
|
911
|
+
return {
|
|
912
|
+
ok: false,
|
|
913
|
+
reason: 'Config "feature" needs "gates": what judges the published line before it is folded (the CI). They are not borrowed from "ticket".',
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
const validate = readReview(object.validate, configDir, "feature.validate", agents);
|
|
917
|
+
if (validate.ok === false) {
|
|
918
|
+
return validate;
|
|
919
|
+
}
|
|
920
|
+
const fix = readGatedPass(object.fix, configDir, "feature.fix", agents);
|
|
921
|
+
if (fix.ok === false) {
|
|
922
|
+
return fix;
|
|
923
|
+
}
|
|
924
|
+
const gates = readGates(object.gates, configDir, "feature.gates");
|
|
925
|
+
if (gates.ok === false) {
|
|
926
|
+
return gates;
|
|
927
|
+
}
|
|
928
|
+
const spec = { fix: fix.value, validate: validate.value, gates: gates.value };
|
|
929
|
+
for (const key of ["maxAttempts", "maxRefusals"]) {
|
|
930
|
+
const read = positiveIntField(object, key);
|
|
931
|
+
if (read.ok === false) {
|
|
932
|
+
return { ok: false, reason: `Config "feature.${key}" must be a positive integer.` };
|
|
933
|
+
}
|
|
934
|
+
if (read.value !== undefined) {
|
|
935
|
+
spec[key] = read.value;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return { ok: true, value: spec };
|
|
939
|
+
}
|
|
671
940
|
/**
|
|
672
941
|
* Flags whose value is a path this system writes to.
|
|
673
942
|
*
|