@bluewombat/runtime 0.9.0 → 0.11.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 +200 -99
- package/SPECS.md +121 -19
- package/dist/cli.js +35 -4
- package/dist/cli.js.map +1 -1
- package/dist/config/agent-shorthand.d.ts +25 -5
- package/dist/config/agent-shorthand.d.ts.map +1 -1
- package/dist/config/agent-shorthand.js +65 -21
- package/dist/config/agent-shorthand.js.map +1 -1
- package/dist/config/fixture-yaml.d.ts +8 -0
- package/dist/config/fixture-yaml.d.ts.map +1 -0
- package/dist/config/fixture-yaml.js +30 -0
- package/dist/config/fixture-yaml.js.map +1 -0
- package/dist/config/home.d.ts +2 -0
- package/dist/config/home.d.ts.map +1 -1
- package/dist/config/home.js +2 -0
- package/dist/config/home.js.map +1 -1
- package/dist/config/load-config.d.ts.map +1 -1
- package/dist/config/load-config.js +301 -62
- 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 +84 -58
- package/dist/config/parse-args.js.map +1 -1
- package/dist/config/types.d.ts +96 -16
- 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 +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/loop/authority.d.ts +38 -1
- package/dist/loop/authority.d.ts.map +1 -1
- package/dist/loop/authority.js +41 -2
- package/dist/loop/authority.js.map +1 -1
- package/dist/loop/context.d.ts +16 -1
- package/dist/loop/context.d.ts.map +1 -1
- package/dist/loop/drive.d.ts +9 -3
- package/dist/loop/drive.d.ts.map +1 -1
- package/dist/loop/drive.js +17 -7
- package/dist/loop/drive.js.map +1 -1
- package/dist/loop/features.d.ts +16 -0
- package/dist/loop/features.d.ts.map +1 -1
- package/dist/loop/features.js +404 -62
- package/dist/loop/features.js.map +1 -1
- package/dist/loop/open-host.d.ts +10 -0
- package/dist/loop/open-host.d.ts.map +1 -1
- package/dist/loop/open-host.js +111 -9
- package/dist/loop/open-host.js.map +1 -1
- package/dist/loop/report.d.ts.map +1 -1
- package/dist/loop/report.js +21 -9
- package/dist/loop/report.js.map +1 -1
- package/dist/loop/streams.d.ts +3 -0
- package/dist/loop/streams.d.ts.map +1 -1
- package/dist/loop/streams.js +2 -1
- package/dist/loop/streams.js.map +1 -1
- package/dist/loop/tick.d.ts +3 -0
- package/dist/loop/tick.d.ts.map +1 -1
- package/dist/loop/tick.js.map +1 -1
- package/dist/loop/trace.d.ts +1 -0
- package/dist/loop/trace.d.ts.map +1 -1
- package/dist/loop/trace.js +2 -2
- package/dist/loop/trace.js.map +1 -1
- package/dist/loop/transformers.d.ts +16 -2
- package/dist/loop/transformers.d.ts.map +1 -1
- package/dist/loop/transformers.js +126 -22
- package/dist/loop/transformers.js.map +1 -1
- package/dist/loop/warm.js +1 -1
- package/dist/loop/warm.js.map +1 -1
- package/dist/operator/agent-plugins.d.ts.map +1 -1
- package/dist/operator/agent-plugins.js +14 -8
- package/dist/operator/agent-plugins.js.map +1 -1
- package/dist/operator/board.d.ts +43 -4
- package/dist/operator/board.d.ts.map +1 -1
- package/dist/operator/board.js +153 -10
- package/dist/operator/board.js.map +1 -1
- package/dist/operator/doctor.d.ts +1 -1
- package/dist/operator/doctor.d.ts.map +1 -1
- package/dist/operator/doctor.js +76 -29
- package/dist/operator/doctor.js.map +1 -1
- package/dist/operator/explain.d.ts.map +1 -1
- package/dist/operator/explain.js +36 -12
- package/dist/operator/explain.js.map +1 -1
- package/dist/operator/film.js +0 -3
- package/dist/operator/film.js.map +1 -1
- package/dist/operator/incident-dossier.d.ts +154 -0
- package/dist/operator/incident-dossier.d.ts.map +1 -0
- package/dist/operator/incident-dossier.js +696 -0
- package/dist/operator/incident-dossier.js.map +1 -0
- package/dist/operator/incident-system.d.ts +48 -0
- package/dist/operator/incident-system.d.ts.map +1 -0
- package/dist/operator/incident-system.js +152 -0
- package/dist/operator/incident-system.js.map +1 -0
- package/dist/operator/incident.d.ts +27 -0
- package/dist/operator/incident.d.ts.map +1 -0
- package/dist/operator/incident.js +220 -0
- package/dist/operator/incident.js.map +1 -0
- package/dist/operator/init.js +27 -22
- package/dist/operator/init.js.map +1 -1
- package/dist/operator/live.js +4 -4
- package/dist/operator/live.js.map +1 -1
- package/dist/operator/log.d.ts +2 -0
- package/dist/operator/log.d.ts.map +1 -1
- package/dist/operator/log.js +7 -3
- package/dist/operator/log.js.map +1 -1
- package/dist/operator/restart.d.ts +20 -0
- package/dist/operator/restart.d.ts.map +1 -0
- package/dist/operator/restart.js +63 -0
- package/dist/operator/restart.js.map +1 -0
- package/dist/operator/setup.d.ts.map +1 -1
- package/dist/operator/setup.js +2 -0
- package/dist/operator/setup.js.map +1 -1
- package/package.json +14 -14
package/dist/config/home.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export declare const HOME_LAYOUT: {
|
|
|
11
11
|
readonly workspaces: "workspaces";
|
|
12
12
|
readonly ledger: "ledger";
|
|
13
13
|
readonly workLine: "work-line";
|
|
14
|
+
readonly targetLine: "target-line";
|
|
15
|
+
readonly featureWorkspaces: "feature-workspaces";
|
|
14
16
|
};
|
|
15
17
|
export declare function homeOf(configDir: string, home: string | undefined): string;
|
|
16
18
|
export declare function inHome(home: string, part: keyof typeof HOME_LAYOUT): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"home.d.ts","sourceRoot":"","sources":["../../src/config/home.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,aAAkB,CAAC;AAE5C,eAAO,MAAM,WAAW
|
|
1
|
+
{"version":3,"file":"home.d.ts","sourceRoot":"","sources":["../../src/config/home.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY,aAAkB,CAAC;AAE5C,eAAO,MAAM,WAAW;;;;;;CAMd,CAAC;AAEX,wBAAgB,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAE1E;AAED,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,WAAW,GAAG,MAAM,CAE3E"}
|
package/dist/config/home.js
CHANGED
|
@@ -13,6 +13,8 @@ export const HOME_LAYOUT = {
|
|
|
13
13
|
workspaces: "workspaces",
|
|
14
14
|
ledger: "ledger",
|
|
15
15
|
workLine: "work-line",
|
|
16
|
+
targetLine: "target-line",
|
|
17
|
+
featureWorkspaces: "feature-workspaces",
|
|
16
18
|
};
|
|
17
19
|
export function homeOf(configDir, home) {
|
|
18
20
|
return resolve(configDir, home ?? DEFAULT_HOME);
|
package/dist/config/home.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"home.js","sourceRoot":"","sources":["../../src/config/home.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,QAAQ,EAAE,CAAC;AAE5C,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,UAAU,EAAE,YAAY;IACxB,MAAM,EAAE,QAAQ;IAChB,QAAQ,EAAE,WAAW;
|
|
1
|
+
{"version":3,"file":"home.js","sourceRoot":"","sources":["../../src/config/home.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAEnD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,QAAQ,EAAE,CAAC;AAE5C,MAAM,CAAC,MAAM,WAAW,GAAG;IACzB,UAAU,EAAE,YAAY;IACxB,MAAM,EAAE,QAAQ;IAChB,QAAQ,EAAE,WAAW;IACrB,UAAU,EAAE,aAAa;IACzB,iBAAiB,EAAE,oBAAoB;CAC/B,CAAC;AAEX,MAAM,UAAU,MAAM,CAAC,SAAiB,EAAE,IAAwB;IAChE,OAAO,OAAO,CAAC,SAAS,EAAE,IAAI,IAAI,YAAY,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,IAA8B;IACjE,OAAO,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;AACvC,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-config.d.ts","sourceRoot":"","sources":["../../src/config/load-config.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"load-config.d.ts","sourceRoot":"","sources":["../../src/config/load-config.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAIV,SAAS,EASV,MAAM,YAAY,CAAC;AAwDpB,MAAM,MAAM,YAAY,GAAG;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,UAAU,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE/F;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAwLrD"}
|
|
@@ -2,28 +2,53 @@ 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
|
-
"featureTracker",
|
|
7
|
-
"ticketTracker",
|
|
8
7
|
"home",
|
|
9
8
|
"workLine",
|
|
10
9
|
"workspaceRoot",
|
|
11
10
|
"ledger",
|
|
12
11
|
"persist",
|
|
13
12
|
"agents",
|
|
14
|
-
"
|
|
15
|
-
"
|
|
13
|
+
"ticket",
|
|
14
|
+
"feature",
|
|
16
15
|
"authority",
|
|
17
16
|
"observability",
|
|
18
17
|
"timeoutMs",
|
|
19
|
-
"maxRefusals",
|
|
20
18
|
"pollIntervalMs",
|
|
21
19
|
]);
|
|
22
|
-
const WORK_LINE_KEYS = new Set([
|
|
20
|
+
const WORK_LINE_KEYS = new Set([
|
|
21
|
+
"stable",
|
|
22
|
+
"targetStable",
|
|
23
|
+
"branch",
|
|
24
|
+
"isolation",
|
|
25
|
+
"isolationOptions",
|
|
26
|
+
"warm",
|
|
27
|
+
]);
|
|
23
28
|
const PASS_KEYS = new Set(["cmd", "timeoutMs"]);
|
|
24
29
|
const GATED_PASS_KEYS = new Set(["cmd", "timeoutMs", "gates"]);
|
|
25
|
-
const STAGE_KEYS = new Set(["producer", "repair", "maxAttempts"]);
|
|
26
|
-
const
|
|
30
|
+
const STAGE_KEYS = new Set(["producer", "repair", "gates", "maxAttempts"]);
|
|
31
|
+
const TICKET_KEYS = new Set([
|
|
32
|
+
"tracker",
|
|
33
|
+
"build",
|
|
34
|
+
"maxParallel",
|
|
35
|
+
"maxAttempts",
|
|
36
|
+
"maxRefusals",
|
|
37
|
+
"fix",
|
|
38
|
+
"validate",
|
|
39
|
+
"gates",
|
|
40
|
+
]);
|
|
41
|
+
const FEATURE_KEYS = new Set([
|
|
42
|
+
"tracker",
|
|
43
|
+
"test",
|
|
44
|
+
"maxAttempts",
|
|
45
|
+
"maxRefusals",
|
|
46
|
+
"fix",
|
|
47
|
+
"validate",
|
|
48
|
+
"gates",
|
|
49
|
+
]);
|
|
50
|
+
/** Keys of a level that name what it builds or tracks, not how its result is judged. */
|
|
51
|
+
const BUILDING_KEYS = new Set(["tracker", "build", "test"]);
|
|
27
52
|
const GATES_KEYS = new Set(["defaultTimeoutMs", "gates"]);
|
|
28
53
|
const AUTHORITY_KEYS = new Set(["enabled", "publish", "refresh", "describe"]);
|
|
29
54
|
const OBSERVABILITY_KEYS = new Set(["streams"]);
|
|
@@ -61,8 +86,15 @@ export function loadConfig(path) {
|
|
|
61
86
|
}
|
|
62
87
|
const configDir = dirname(file);
|
|
63
88
|
const invocation = { configDir };
|
|
64
|
-
for (const side of [
|
|
65
|
-
|
|
89
|
+
for (const [level, side] of [
|
|
90
|
+
["feature", "featureTracker"],
|
|
91
|
+
["ticket", "ticketTracker"],
|
|
92
|
+
]) {
|
|
93
|
+
const block = object[level];
|
|
94
|
+
const raw = block !== null && typeof block === "object" && !Array.isArray(block)
|
|
95
|
+
? block.tracker
|
|
96
|
+
: undefined;
|
|
97
|
+
const spec = readManagerSpec(raw, `${level}.tracker`);
|
|
66
98
|
if (spec.ok === false) {
|
|
67
99
|
return spec;
|
|
68
100
|
}
|
|
@@ -85,6 +117,9 @@ export function loadConfig(path) {
|
|
|
85
117
|
if (workLine.stable !== undefined) {
|
|
86
118
|
invocation.workLineStable = workLine.stable;
|
|
87
119
|
}
|
|
120
|
+
if (workLine.targetStable !== undefined) {
|
|
121
|
+
invocation.workLineTargetStable = workLine.targetStable;
|
|
122
|
+
}
|
|
88
123
|
if (workLine.branch !== undefined) {
|
|
89
124
|
invocation.workLineBranch = workLine.branch;
|
|
90
125
|
}
|
|
@@ -127,19 +162,35 @@ export function loadConfig(path) {
|
|
|
127
162
|
}
|
|
128
163
|
agents = read.value;
|
|
129
164
|
}
|
|
130
|
-
if (object.
|
|
131
|
-
const
|
|
132
|
-
if (
|
|
133
|
-
return
|
|
165
|
+
if (object.ticket !== undefined) {
|
|
166
|
+
const ticket = readTicket(object.ticket, configDir, agents);
|
|
167
|
+
if (ticket.ok === false) {
|
|
168
|
+
return ticket;
|
|
169
|
+
}
|
|
170
|
+
if (ticket.builder !== undefined) {
|
|
171
|
+
invocation.builder = ticket.builder;
|
|
172
|
+
}
|
|
173
|
+
if (ticket.value !== undefined) {
|
|
174
|
+
invocation.ticket = ticket.value;
|
|
134
175
|
}
|
|
135
|
-
invocation.builder = builder.value;
|
|
136
176
|
}
|
|
137
|
-
if (object.
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
return
|
|
177
|
+
if (object.feature !== undefined) {
|
|
178
|
+
const feature = readFeature(object.feature, configDir, agents);
|
|
179
|
+
if (feature.ok === false) {
|
|
180
|
+
return feature;
|
|
181
|
+
}
|
|
182
|
+
if (feature.test !== undefined) {
|
|
183
|
+
if (invocation.builder === undefined) {
|
|
184
|
+
return {
|
|
185
|
+
ok: false,
|
|
186
|
+
reason: 'Config "feature.test" needs "ticket.build": the Ticket that holds the suite is made like any other.',
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
invocation.builder.featureTest = feature.test;
|
|
190
|
+
}
|
|
191
|
+
if (feature.value !== undefined) {
|
|
192
|
+
invocation.feature = feature.value;
|
|
141
193
|
}
|
|
142
|
-
invocation.assembly = assembly.value;
|
|
143
194
|
}
|
|
144
195
|
if (object.authority !== undefined) {
|
|
145
196
|
const authority = readAuthority(object.authority, configDir, agents);
|
|
@@ -162,15 +213,6 @@ export function loadConfig(path) {
|
|
|
162
213
|
if (timeoutMs.value !== undefined) {
|
|
163
214
|
invocation.timeoutMs = timeoutMs.value;
|
|
164
215
|
}
|
|
165
|
-
for (const key of ["maxRefusals"]) {
|
|
166
|
-
const read = positiveIntField(object, key);
|
|
167
|
-
if (read.ok === false) {
|
|
168
|
-
return read;
|
|
169
|
-
}
|
|
170
|
-
if (read.value !== undefined) {
|
|
171
|
-
invocation[key] = read.value;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
216
|
const pollIntervalMs = positiveIntField(object, "pollIntervalMs");
|
|
175
217
|
if (pollIntervalMs.ok === false) {
|
|
176
218
|
return pollIntervalMs;
|
|
@@ -178,8 +220,47 @@ export function loadConfig(path) {
|
|
|
178
220
|
if (pollIntervalMs.value !== undefined) {
|
|
179
221
|
invocation.pollIntervalMs = pollIntervalMs.value;
|
|
180
222
|
}
|
|
223
|
+
const featureTestRefusal = checkFeatureTestDeclaration(invocation);
|
|
224
|
+
if (featureTestRefusal !== undefined) {
|
|
225
|
+
return { ok: false, reason: featureTestRefusal };
|
|
226
|
+
}
|
|
181
227
|
return { ok: true, invocation };
|
|
182
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* The feature-test check is declared by a pair: `feature.test`, which makes the Ticket that holds the
|
|
231
|
+
* suite, and a Feature Gate with `skipWhen: skip-feature-test`, which runs it. One without the other
|
|
232
|
+
* is a check that never runs or a Ticket nobody judges, so neither is accepted alone. A `skipWhen`
|
|
233
|
+
* outside a Feature's Gates has nothing to read.
|
|
234
|
+
*/
|
|
235
|
+
function checkFeatureTestDeclaration(invocation) {
|
|
236
|
+
const outside = [
|
|
237
|
+
["ticket.build.gates", invocation.builder?.producer.gates],
|
|
238
|
+
["feature.test.gates", invocation.builder?.featureTest?.gates],
|
|
239
|
+
["ticket.gates", invocation.ticket?.gates],
|
|
240
|
+
["ticket.fix.gates", invocation.ticket?.fix?.gates],
|
|
241
|
+
["ticket.validate.gates", invocation.ticket?.validate?.gates],
|
|
242
|
+
];
|
|
243
|
+
for (const [key, gates] of outside) {
|
|
244
|
+
const bad = gates?.find((gate) => gate.skipWhen !== undefined);
|
|
245
|
+
if (bad !== undefined) {
|
|
246
|
+
return `Config "${key}" gate "${bad.id}" has "skipWhen", which only a Feature's gates (feature.validate, feature.gates, feature.fix) can have.`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const feature = invocation.feature;
|
|
250
|
+
const skippable = [
|
|
251
|
+
...(feature?.validate.gates ?? []),
|
|
252
|
+
...(feature?.gates ?? []),
|
|
253
|
+
...(feature?.fix.gates ?? []),
|
|
254
|
+
].filter((gate) => gate.skipWhen === "skip-feature-test");
|
|
255
|
+
const declared = invocation.builder?.featureTest !== undefined;
|
|
256
|
+
if (skippable.length > 0 && !declared) {
|
|
257
|
+
return `Config gate "${skippable[0]?.id}" has "skipWhen: skip-feature-test", so the feature-test check is declared: "feature.test" is required (the pass that makes the Ticket holding the suite).`;
|
|
258
|
+
}
|
|
259
|
+
if (declared && feature !== undefined && skippable.length === 0) {
|
|
260
|
+
return 'Config "feature.test" is declared, but no Feature gate runs the suite. Add a gate to "feature.validate.gates" with "skipWhen: skip-feature-test".';
|
|
261
|
+
}
|
|
262
|
+
return undefined;
|
|
263
|
+
}
|
|
183
264
|
const MANAGER_SPEC_KEYS = new Set(["package", "options"]);
|
|
184
265
|
function readManagerSpec(value, side) {
|
|
185
266
|
if (value === undefined) {
|
|
@@ -257,6 +338,10 @@ function readWorkLine(value, configDir) {
|
|
|
257
338
|
if (stable.ok === false) {
|
|
258
339
|
return { ok: false, reason: `Config "workLine.stable" must be a non-empty string.` };
|
|
259
340
|
}
|
|
341
|
+
const targetStable = stringField(object, "targetStable");
|
|
342
|
+
if (targetStable.ok === false) {
|
|
343
|
+
return { ok: false, reason: `Config "workLine.targetStable" must be a non-empty string.` };
|
|
344
|
+
}
|
|
260
345
|
const branch = stringField(object, "branch");
|
|
261
346
|
if (branch.ok === false) {
|
|
262
347
|
return { ok: false, reason: `Config "workLine.branch" must be a non-empty string.` };
|
|
@@ -286,6 +371,9 @@ function readWorkLine(value, configDir) {
|
|
|
286
371
|
return {
|
|
287
372
|
ok: true,
|
|
288
373
|
...(stable.value === undefined ? {} : { stable: resolve(configDir, stable.value) }),
|
|
374
|
+
...(targetStable.value === undefined
|
|
375
|
+
? {}
|
|
376
|
+
: { targetStable: resolve(configDir, targetStable.value) }),
|
|
289
377
|
...(branch.value === undefined ? {} : { branch: branch.value }),
|
|
290
378
|
...(isolation.value === undefined ? {} : { isolation: isolation.value }),
|
|
291
379
|
...(isolationOptions === undefined
|
|
@@ -457,8 +545,9 @@ function readGates(value, configDir, key) {
|
|
|
457
545
|
if (fallback.ok === false) {
|
|
458
546
|
return { ok: false, reason: `Config "${key}.defaultTimeoutMs" must be a positive integer.` };
|
|
459
547
|
}
|
|
548
|
+
// A bare `gates:` in YAML is a null: an empty list, written the short way.
|
|
460
549
|
const entries = object.gates;
|
|
461
|
-
if (entries === undefined) {
|
|
550
|
+
if (entries === undefined || entries === null) {
|
|
462
551
|
return { ok: true, value: [] };
|
|
463
552
|
}
|
|
464
553
|
if (!Array.isArray(entries)) {
|
|
@@ -496,7 +585,19 @@ function readGates(value, configDir, key) {
|
|
|
496
585
|
reason: `Config gate "${id}" has no "timeoutMs", and "${key}" names no "defaultTimeoutMs".`,
|
|
497
586
|
};
|
|
498
587
|
}
|
|
499
|
-
|
|
588
|
+
const skipWhen = entry.skipWhen;
|
|
589
|
+
if (skipWhen !== undefined && !SKIP_WHEN.includes(skipWhen)) {
|
|
590
|
+
return {
|
|
591
|
+
ok: false,
|
|
592
|
+
reason: `Config gate "${id}" has "skipWhen: ${String(skipWhen)}". Known: ${SKIP_WHEN.join(", ")}.`,
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
gates.push({
|
|
596
|
+
id,
|
|
597
|
+
argv: resolveArgv(argv, configDir),
|
|
598
|
+
timeoutMs,
|
|
599
|
+
...(skipWhen === undefined ? {} : { skipWhen: skipWhen }),
|
|
600
|
+
});
|
|
500
601
|
}
|
|
501
602
|
return { ok: true, value: gates };
|
|
502
603
|
}
|
|
@@ -505,7 +606,7 @@ function readGates(value, configDir, key) {
|
|
|
505
606
|
*
|
|
506
607
|
* `timeoutMs` is required: without it nothing bounds an agent, and an agent
|
|
507
608
|
* that never returns is the failure this shape exists to prevent. `repair` and
|
|
508
|
-
* `
|
|
609
|
+
* `ticket` inherit both the command and the ceiling, so a Project with one
|
|
509
610
|
* agent writes one command.
|
|
510
611
|
*/
|
|
511
612
|
function readPass(value, configDir, key) {
|
|
@@ -538,12 +639,13 @@ function readPass(value, configDir, key) {
|
|
|
538
639
|
};
|
|
539
640
|
}
|
|
540
641
|
/**
|
|
541
|
-
* A command, and its own Gate sequence — `
|
|
542
|
-
* `
|
|
543
|
-
*
|
|
544
|
-
*
|
|
642
|
+
* A command, and its own Gate sequence — `ticket.fix`, `ticket.validate`.
|
|
643
|
+
* `ticket.build.producer` and `ticket.build.repair` read the same shape with `gated`
|
|
644
|
+
* false: their Gates are the one `ticket.build.gates` list, read by `readBuilder`.
|
|
645
|
+
* Deliberately its own ~10 lines rather than a parameterized `readPass`: each
|
|
646
|
+
* reader's error messages and key set stay independently legible.
|
|
545
647
|
*/
|
|
546
|
-
function readGatedPass(value, configDir, key, agents) {
|
|
648
|
+
function readGatedPass(value, configDir, key, agents, gated = true) {
|
|
547
649
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
548
650
|
return {
|
|
549
651
|
ok: false,
|
|
@@ -553,6 +655,13 @@ function readGatedPass(value, configDir, key, agents) {
|
|
|
553
655
|
const object = value;
|
|
554
656
|
const short = object.agent !== undefined;
|
|
555
657
|
for (const name of Object.keys(object)) {
|
|
658
|
+
if (!gated && name === "gates") {
|
|
659
|
+
return {
|
|
660
|
+
ok: false,
|
|
661
|
+
reason: `Config "${key}" has no "gates". The builder's Gates are "ticket.build.gates": ` +
|
|
662
|
+
"one list, judging the producer and the repair alike.",
|
|
663
|
+
};
|
|
664
|
+
}
|
|
556
665
|
const known = GATED_PASS_KEYS.has(name) || (short && SHORT_PASS_KEYS.has(name));
|
|
557
666
|
if (!known) {
|
|
558
667
|
return { ok: false, reason: `Config "${key}" has unknown key "${name}".` };
|
|
@@ -593,40 +702,84 @@ function readGatedPass(value, configDir, key, agents) {
|
|
|
593
702
|
}
|
|
594
703
|
return { ok: true, value: { cmd, timeoutMs: timeoutMs.value, gates } };
|
|
595
704
|
}
|
|
705
|
+
/**
|
|
706
|
+
* A review: an agent and its Gates, or `agent: false` and the Gates alone.
|
|
707
|
+
* The agent is turned off by saying so; a review with neither an agent nor a
|
|
708
|
+
* Gate would judge nothing, and is refused.
|
|
709
|
+
*/
|
|
710
|
+
function readReview(value, configDir, key, agents) {
|
|
711
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
712
|
+
return {
|
|
713
|
+
ok: false,
|
|
714
|
+
reason: `Config "${key}" must be a YAML mapping with an agent (or "cmd") and "timeoutMs", or "agent: false" and "gates".`,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
const object = value;
|
|
718
|
+
if (object.agent !== false) {
|
|
719
|
+
return readGatedPass(value, configDir, key, agents);
|
|
720
|
+
}
|
|
721
|
+
for (const name of Object.keys(object)) {
|
|
722
|
+
if (name !== "agent" && name !== "gates") {
|
|
723
|
+
return {
|
|
724
|
+
ok: false,
|
|
725
|
+
reason: `Config "${key}" has "agent: false", so it has no "${name}": only its "gates" run.`,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
// No Gates either: the review is off, which is how a Project says it.
|
|
730
|
+
if (object.gates === undefined || object.gates === null) {
|
|
731
|
+
return { ok: true, value: { agent: false, gates: [] } };
|
|
732
|
+
}
|
|
733
|
+
const gates = readGates(object.gates, configDir, `${key}.gates`);
|
|
734
|
+
if (gates.ok === false) {
|
|
735
|
+
return gates;
|
|
736
|
+
}
|
|
737
|
+
return { ok: true, value: { agent: false, gates: gates.value } };
|
|
738
|
+
}
|
|
596
739
|
/**
|
|
597
740
|
* Making the unit of work: both passes named, neither borrowing the other's command.
|
|
598
741
|
*/
|
|
599
742
|
function readBuilder(value, configDir, agents) {
|
|
600
743
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
601
|
-
return { ok: false, reason: 'Config "
|
|
744
|
+
return { ok: false, reason: 'Config "ticket.build" must be a YAML mapping.' };
|
|
602
745
|
}
|
|
603
746
|
const object = value;
|
|
604
747
|
for (const name of Object.keys(object)) {
|
|
605
748
|
if (!STAGE_KEYS.has(name)) {
|
|
606
|
-
return { ok: false, reason: `Config "
|
|
749
|
+
return { ok: false, reason: `Config "ticket.build" has unknown key "${name}".` };
|
|
607
750
|
}
|
|
608
751
|
}
|
|
609
752
|
if (object.producer === undefined) {
|
|
610
|
-
return { ok: false, reason: 'Config "
|
|
753
|
+
return { ok: false, reason: 'Config "ticket.build" needs a "producer".' };
|
|
611
754
|
}
|
|
612
755
|
if (object.repair === undefined) {
|
|
613
|
-
return { ok: false, reason: 'Config "
|
|
756
|
+
return { ok: false, reason: 'Config "ticket.build" needs a "repair".' };
|
|
614
757
|
}
|
|
615
|
-
const readProducer = readGatedPass(object.producer, configDir, "
|
|
758
|
+
const readProducer = readGatedPass(object.producer, configDir, "ticket.build.producer", agents, false);
|
|
616
759
|
if (readProducer.ok === false) {
|
|
617
760
|
return readProducer;
|
|
618
761
|
}
|
|
619
|
-
const readRepair = readGatedPass(object.repair, configDir, "
|
|
762
|
+
const readRepair = readGatedPass(object.repair, configDir, "ticket.build.repair", agents, false);
|
|
620
763
|
if (readRepair.ok === false) {
|
|
621
764
|
return readRepair;
|
|
622
765
|
}
|
|
766
|
+
let gates = [];
|
|
767
|
+
if (object.gates !== undefined) {
|
|
768
|
+
const read = readGates(object.gates, configDir, "ticket.build.gates");
|
|
769
|
+
if (read.ok === false) {
|
|
770
|
+
return read;
|
|
771
|
+
}
|
|
772
|
+
gates = read.value;
|
|
773
|
+
}
|
|
774
|
+
// One list, two passes: a repair is judged by exactly what judged the producer,
|
|
775
|
+
// so it cannot undo what a Gate refused. Implementer still takes two sequences.
|
|
623
776
|
const spec = {
|
|
624
|
-
producer: readProducer.value,
|
|
625
|
-
repair: readRepair.value,
|
|
777
|
+
producer: { ...readProducer.value, gates },
|
|
778
|
+
repair: { ...readRepair.value, gates: [...gates] },
|
|
626
779
|
};
|
|
627
780
|
const maxAttempts = positiveIntField(object, "maxAttempts");
|
|
628
781
|
if (maxAttempts.ok === false) {
|
|
629
|
-
return { ok: false, reason: 'Config "
|
|
782
|
+
return { ok: false, reason: 'Config "ticket.build.maxAttempts" must be a positive integer.' };
|
|
630
783
|
}
|
|
631
784
|
if (maxAttempts.value !== undefined) {
|
|
632
785
|
spec.maxAttempts = maxAttempts.value;
|
|
@@ -637,53 +790,139 @@ function readBuilder(value, configDir, agents) {
|
|
|
637
790
|
* Judging the assembled ticket. A command here is a fix of what a judgement
|
|
638
791
|
* refused, not a second first-pass of the request.
|
|
639
792
|
*/
|
|
640
|
-
function
|
|
793
|
+
function readTicket(value, configDir, agents) {
|
|
641
794
|
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
642
|
-
return { ok: false, reason: 'Config "
|
|
795
|
+
return { ok: false, reason: 'Config "ticket" must be a YAML mapping.' };
|
|
643
796
|
}
|
|
644
797
|
const object = value;
|
|
645
798
|
for (const name of Object.keys(object)) {
|
|
646
799
|
if (name === "producer" || name === "repair") {
|
|
647
800
|
return {
|
|
648
801
|
ok: false,
|
|
649
|
-
reason: `Config "
|
|
650
|
-
'("
|
|
802
|
+
reason: `Config "ticket" has no "${name}". The assembled ticket is judged ` +
|
|
803
|
+
'("ticket.gates"); a refusal is "ticket.fix".',
|
|
651
804
|
};
|
|
652
805
|
}
|
|
653
|
-
if (!
|
|
654
|
-
return { ok: false, reason: `Config "
|
|
806
|
+
if (!TICKET_KEYS.has(name)) {
|
|
807
|
+
return { ok: false, reason: `Config "ticket" has unknown key "${name}".` };
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
let builder;
|
|
811
|
+
if (object.build !== undefined) {
|
|
812
|
+
const read = readBuilder(object.build, configDir, agents);
|
|
813
|
+
if (read.ok === false) {
|
|
814
|
+
return read;
|
|
655
815
|
}
|
|
816
|
+
builder = read.value;
|
|
817
|
+
}
|
|
818
|
+
if (Object.keys(object).every((name) => BUILDING_KEYS.has(name))) {
|
|
819
|
+
return { ok: true, ...(builder === undefined ? {} : { builder }) };
|
|
820
|
+
}
|
|
821
|
+
if (object.validate === undefined) {
|
|
822
|
+
return {
|
|
823
|
+
ok: false,
|
|
824
|
+
reason: 'Config "ticket" needs a "validate": the review of the assembled ticket. To turn it off, write "validate: { agent: false }".',
|
|
825
|
+
};
|
|
656
826
|
}
|
|
657
827
|
const spec = { gates: [] };
|
|
658
828
|
if (object.fix !== undefined) {
|
|
659
|
-
const read = readGatedPass(object.fix, configDir, "
|
|
829
|
+
const read = readGatedPass(object.fix, configDir, "ticket.fix", agents);
|
|
660
830
|
if (read.ok === false) {
|
|
661
831
|
return read;
|
|
662
832
|
}
|
|
663
833
|
spec.fix = read.value;
|
|
664
834
|
}
|
|
665
835
|
if (object.validate !== undefined) {
|
|
666
|
-
const read =
|
|
836
|
+
const read = readReview(object.validate, configDir, "ticket.validate", agents);
|
|
667
837
|
if (read.ok === false) {
|
|
668
838
|
return read;
|
|
669
839
|
}
|
|
670
840
|
spec.validate = read.value;
|
|
671
841
|
}
|
|
672
|
-
const
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
842
|
+
for (const key of ["maxParallel", "maxAttempts", "maxRefusals"]) {
|
|
843
|
+
const read = positiveIntField(object, key);
|
|
844
|
+
if (read.ok === false) {
|
|
845
|
+
return { ok: false, reason: `Config "ticket.${key}" must be a positive integer.` };
|
|
846
|
+
}
|
|
847
|
+
if (read.value !== undefined) {
|
|
848
|
+
spec[key] = read.value;
|
|
849
|
+
}
|
|
678
850
|
}
|
|
679
851
|
if (object.gates !== undefined) {
|
|
680
|
-
const gates = readGates(object.gates, configDir, "
|
|
852
|
+
const gates = readGates(object.gates, configDir, "ticket.gates");
|
|
681
853
|
if (gates.ok === false) {
|
|
682
854
|
return gates;
|
|
683
855
|
}
|
|
684
856
|
spec.gates = gates.value;
|
|
685
857
|
}
|
|
686
|
-
return { ok: true, value: spec };
|
|
858
|
+
return { ok: true, value: spec, ...(builder === undefined ? {} : { builder }) };
|
|
859
|
+
}
|
|
860
|
+
/**
|
|
861
|
+
* Judging a Feature's whole line, and repairing what that judgement refused.
|
|
862
|
+
* Its own gates, fix and budget: nothing is shared with a Ticket's.
|
|
863
|
+
*/
|
|
864
|
+
function readFeature(value, configDir, agents) {
|
|
865
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
866
|
+
return { ok: false, reason: 'Config "feature" must be a YAML mapping.' };
|
|
867
|
+
}
|
|
868
|
+
const object = value;
|
|
869
|
+
for (const name of Object.keys(object)) {
|
|
870
|
+
if (!FEATURE_KEYS.has(name)) {
|
|
871
|
+
return { ok: false, reason: `Config "feature" has unknown key "${name}".` };
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
let test;
|
|
875
|
+
if (object.test !== undefined) {
|
|
876
|
+
const read = readGatedPass(object.test, configDir, "feature.test", agents);
|
|
877
|
+
if (read.ok === false) {
|
|
878
|
+
return read;
|
|
879
|
+
}
|
|
880
|
+
test = read.value;
|
|
881
|
+
}
|
|
882
|
+
if (Object.keys(object).every((name) => BUILDING_KEYS.has(name))) {
|
|
883
|
+
return { ok: true, ...(test === undefined ? {} : { test }) };
|
|
884
|
+
}
|
|
885
|
+
if (object.fix === undefined) {
|
|
886
|
+
return {
|
|
887
|
+
ok: false,
|
|
888
|
+
reason: 'Config "feature" needs a "fix": what repairs the line when its gates refuse it. There is no feature without one.',
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
if (object.validate === undefined) {
|
|
892
|
+
return {
|
|
893
|
+
ok: false,
|
|
894
|
+
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.',
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
if (object.gates === undefined) {
|
|
898
|
+
return {
|
|
899
|
+
ok: false,
|
|
900
|
+
reason: 'Config "feature" needs "gates": what judges the published line before it is folded (the CI). They are not borrowed from "ticket".',
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
const validate = readReview(object.validate, configDir, "feature.validate", agents);
|
|
904
|
+
if (validate.ok === false) {
|
|
905
|
+
return validate;
|
|
906
|
+
}
|
|
907
|
+
const fix = readGatedPass(object.fix, configDir, "feature.fix", agents);
|
|
908
|
+
if (fix.ok === false) {
|
|
909
|
+
return fix;
|
|
910
|
+
}
|
|
911
|
+
const gates = readGates(object.gates, configDir, "feature.gates");
|
|
912
|
+
if (gates.ok === false) {
|
|
913
|
+
return gates;
|
|
914
|
+
}
|
|
915
|
+
const spec = { fix: fix.value, validate: validate.value, gates: gates.value };
|
|
916
|
+
for (const key of ["maxAttempts", "maxRefusals"]) {
|
|
917
|
+
const read = positiveIntField(object, key);
|
|
918
|
+
if (read.ok === false) {
|
|
919
|
+
return { ok: false, reason: `Config "feature.${key}" must be a positive integer.` };
|
|
920
|
+
}
|
|
921
|
+
if (read.value !== undefined) {
|
|
922
|
+
spec[key] = read.value;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
return { ok: true, value: spec, ...(test === undefined ? {} : { test }) };
|
|
687
926
|
}
|
|
688
927
|
/**
|
|
689
928
|
* Flags whose value is a path this system writes to.
|