tickmarkr 2.7.6 → 2.7.8
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/dist/compile/native.js +9 -7
- package/dist/compile/prd.js +4 -3
- package/dist/compile/speckit.js +3 -2
- package/dist/gates/acceptance.js +5 -4
- package/dist/gates/artifact-manifest.js +17 -13
- package/dist/gates/git-paths.d.ts +6 -0
- package/dist/gates/git-paths.js +22 -0
- package/dist/run/beat-lifecycle.js +1 -1
- package/dist/run/daemon.js +2 -2
- package/dist/run/launchservices-check.js +1 -1
- package/dist/run/lease.js +2 -2
- package/dist/run/stall.js +3 -2
- package/dist/run/suite-census.js +2 -2
- package/package.json +2 -1
- package/skills/tickmarkr-overseer/SKILL.md +6 -3
- package/skills/tickmarkr-overseer/scripts/grade-ci.sh +159 -42
package/dist/compile/native.js
CHANGED
|
@@ -179,12 +179,14 @@ export const COLLECTABLE_TESTS = "tests/**/*.test.ts";
|
|
|
179
179
|
export const LEGACY_PREFIX = ["dro", "vr"].join("");
|
|
180
180
|
export const TICKMARKR_NATIVE_MARKER = /^<!--\s*tickmarkr:spec(?:\s+v1)?\s*-->\s*$/m;
|
|
181
181
|
export const NATIVE_MARKER = new RegExp(`^<!--\\s*(?:tickmarkr|${LEGACY_PREFIX}):spec(?:\\s+v1)?\\s*-->\\s*$`, "m");
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
182
|
+
// Queue row 130: the rest of a spec line is [^\r\n], never `.` — `.` misses U+2028/U+2029, and an acceptance item holding
|
|
183
|
+
// one was glued onto its neighbour as a plain judge string, losing its own test: oracle (PRD and Spec Kit dropped it).
|
|
184
|
+
const HEAD_RE = /^## (T\d+):\s*([^\r\n]+)$/;
|
|
185
|
+
const FIELD_RE = /^- (\w+):\s*([^\r\n]*)$/;
|
|
186
|
+
const NESTED_RE = /^\s+- ([^\r\n]+)$/;
|
|
185
187
|
// v1.19: a typed acceptance oracle line — "command: ...", "test: ...", or "judge: ...". Anything
|
|
186
188
|
// without one of these prefixes is a plain-string judge criterion (compat path, emits a warning).
|
|
187
|
-
const ORACLE_RE = new RegExp(`^(${ORACLES.join("|")}):\\s*(
|
|
189
|
+
const ORACLE_RE = new RegExp(`^(${ORACLES.join("|")}):\\s*([^\\r\\n]*)$`);
|
|
188
190
|
const FIELDS = new Set(["goal", "shape", "deps", "files", "context", "complexity", "humangate", "pin", "floor", "gates", "acceptance", "timeout", "pins", "outofscope"]);
|
|
189
191
|
const listItems = (draft) => draft.list === "acceptance" ? draft.acceptanceRaw
|
|
190
192
|
: draft.list === "pins" ? draft.pinsRaw
|
|
@@ -199,7 +201,7 @@ const PIN_GLOB_SEP = "| glob:";
|
|
|
199
201
|
const LANDING_SEP = "| suite:";
|
|
200
202
|
function parsePin(task, raw, index, pathSet) {
|
|
201
203
|
const bad = (detail) => invalid(task, "pins", `item ${index + 1} (${JSON.stringify(raw)}) ${detail}`);
|
|
202
|
-
const typed = raw.match(/^(\w+):\s*(
|
|
204
|
+
const typed = raw.match(/^(\w+):\s*([^\r\n]*)$/);
|
|
203
205
|
if (typed?.[1] === "fixture") {
|
|
204
206
|
const paths = pathSet(typed[2]);
|
|
205
207
|
if (!paths.length)
|
|
@@ -275,7 +277,7 @@ function testsAtHead(root) {
|
|
|
275
277
|
}
|
|
276
278
|
const bodies = new Map();
|
|
277
279
|
for (const line of grep.stdout.split("\n")) {
|
|
278
|
-
const match = line.match(/^HEAD:(tests
|
|
280
|
+
const match = line.match(/^HEAD:(tests\/[^\r\n]*\.test\.ts):\d+:([^\r\n]*)$/);
|
|
279
281
|
if (!match)
|
|
280
282
|
continue;
|
|
281
283
|
const lines = bodies.get(match[1]) ?? [];
|
|
@@ -544,7 +546,7 @@ export function compileNative(file, options = {}) {
|
|
|
544
546
|
}
|
|
545
547
|
const draft = drafts.at(-1);
|
|
546
548
|
if (!draft) {
|
|
547
|
-
const base = line.match(/^base:\s*(
|
|
549
|
+
const base = line.match(/^base:\s*([^\r\n]*)$/);
|
|
548
550
|
if (base) {
|
|
549
551
|
const value = base[1].trim();
|
|
550
552
|
if (!value) {
|
package/dist/compile/prd.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { validateGraph } from "../graph/schema.js";
|
|
3
3
|
import { CompileError, inferShape, sha256 } from "./common.js";
|
|
4
|
-
|
|
5
|
-
const
|
|
6
|
-
const
|
|
4
|
+
// Queue row 130: [^\r\n], never `.` (U+2028/U+2029 inside a line).
|
|
5
|
+
const HEAD_RE = /^## (T\d+):\s*([^\r\n]+)$/;
|
|
6
|
+
const FIELD_RE = /^- (\w+):\s*([^\r\n]*)$/;
|
|
7
|
+
const NESTED_RE = /^\s+- ([^\r\n]+)$/;
|
|
7
8
|
export function compilePrd(file) {
|
|
8
9
|
if (!existsSync(file))
|
|
9
10
|
throw new CompileError(`no such PRD file: ${file}`);
|
package/dist/compile/speckit.js
CHANGED
|
@@ -2,8 +2,9 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { validateGraph } from "../graph/schema.js";
|
|
4
4
|
import { CompileError, inferShape, sha256 } from "./common.js";
|
|
5
|
-
|
|
6
|
-
const
|
|
5
|
+
// Queue row 130: [^\r\n], never `.` (U+2028/U+2029 inside a line).
|
|
6
|
+
const TASK_RE = /^- \[( |x)\] (T\d+)\s*(\[P\])?\s*([^\r\n]+)$/;
|
|
7
|
+
const SUB_RE = /^\s+- (\w+):\s*([^\r\n]+)$/;
|
|
7
8
|
export function compileSpecKit(dir) {
|
|
8
9
|
const path = join(dir, "tasks.md");
|
|
9
10
|
if (!existsSync(path)) {
|
package/dist/gates/acceptance.js
CHANGED
|
@@ -307,13 +307,14 @@ function compressLines(lines) {
|
|
|
307
307
|
}
|
|
308
308
|
return parts.join(", ");
|
|
309
309
|
}
|
|
310
|
-
|
|
310
|
+
// Queue rows 128/129: "\n"-only line semantics — the m flag would also split a line at U+2028/U+2029.
|
|
311
|
+
const DIFF_SECTIONS = /(?<=^|\n)(?=diff --git )/;
|
|
311
312
|
// the a-side path of a whole-file deletion section, or null if this section is not one.
|
|
312
313
|
function deletedPath(section) {
|
|
313
|
-
if (
|
|
314
|
+
if (!/(?<=^|\n)deleted file mode /.test(section))
|
|
314
315
|
return null;
|
|
315
|
-
const oldPath =
|
|
316
|
-
??
|
|
316
|
+
const oldPath = /(?<=^|\n)--- ([^\r\n]+)/.exec(section)?.[1]
|
|
317
|
+
?? /(?<=^|\n)Binary files ([^\r\n]+) and \/dev\/null differ(?=[\r\n]|$)/.exec(section)?.[1];
|
|
317
318
|
if (!oldPath || oldPath === "/dev/null")
|
|
318
319
|
return null;
|
|
319
320
|
const unquoted = oldPath.startsWith('"') && oldPath.endsWith('"') ? oldPath.slice(1, -1) : oldPath;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* path receives capture accounting only when its manifest row names a
|
|
4
4
|
* registered producer and repeats that producer's current provenance exactly.
|
|
5
5
|
*/
|
|
6
|
-
import { diffSidePath, gitHeaderPaths, unquoteGitPath } from "./git-paths.js";
|
|
6
|
+
import { diffSidePath, gitHeaderPaths, quoteGitPath, unquoteGitPath } from "./git-paths.js";
|
|
7
7
|
const GOLDEN_PROVENANCE = {
|
|
8
8
|
source: "src/tui/cockpit/capture.ts",
|
|
9
9
|
entrypoint: "regenerateGoldenFrames",
|
|
@@ -191,17 +191,21 @@ export function classifyArtifactPath(path, manifest = CAPTURE_ARTIFACT_MANIFEST)
|
|
|
191
191
|
provenance: producer.provenance,
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
|
-
|
|
194
|
+
// Queue rows 128/129: patch text is split into lines at "\n" only. Under the m flag JS `^` and `$` also break at
|
|
195
|
+
// U+2028/U+2029 (and `.` never matches them), so an added line holding one could forge a header line; these patterns
|
|
196
|
+
// anchor on "\n" instead, and a name written back into the text is quoted (quoteGitPath) so it cannot end a line.
|
|
197
|
+
const SET_ASIDE_RECEIPT = /(?<=^|\n)set aside: regenerable capture ([^\r\n]+?) — \d+ bytes withheld\b/;
|
|
195
198
|
/** The path named by a citable capture receipt, or null when there is none. */
|
|
196
199
|
export function setAsideReceiptPath(section) {
|
|
197
|
-
|
|
200
|
+
const path = SET_ASIDE_RECEIPT.exec(section)?.[1];
|
|
201
|
+
return path === undefined ? null : unquoteGitPath(path);
|
|
198
202
|
}
|
|
199
|
-
const DIFF_SECTIONS = /(
|
|
203
|
+
const DIFF_SECTIONS = /(?<=^|\n)(?=diff --git )/;
|
|
200
204
|
function deletedPath(section) {
|
|
201
|
-
if (
|
|
205
|
+
if (!/(?<=^|\n)deleted file mode /.test(section))
|
|
202
206
|
return null;
|
|
203
|
-
const oldPath =
|
|
204
|
-
??
|
|
207
|
+
const oldPath = /(?<=^|\n)--- ([^\r\n]+)/.exec(section)?.[1]
|
|
208
|
+
?? /(?<=^|\n)Binary files ([^\r\n]+) and \/dev\/null differ(?=[\r\n]|$)/.exec(section)?.[1];
|
|
205
209
|
return oldPath ? diffSidePath(oldPath) : null;
|
|
206
210
|
}
|
|
207
211
|
/**
|
|
@@ -216,7 +220,7 @@ export function reviewableLogicDiff(diff) {
|
|
|
216
220
|
const path = deletedPath(section);
|
|
217
221
|
if (!path || isProtectedEvidence(path))
|
|
218
222
|
return section;
|
|
219
|
-
return `deleted file: ${path}\n`;
|
|
223
|
+
return `deleted file: ${quoteGitPath(path)}\n`;
|
|
220
224
|
}).join("");
|
|
221
225
|
}
|
|
222
226
|
function parseContentSection(section) {
|
|
@@ -240,12 +244,12 @@ function parseContentSection(section) {
|
|
|
240
244
|
function sectionPaths(section, parsed) {
|
|
241
245
|
if (parsed)
|
|
242
246
|
return [...new Set(parsed.sides.filter((path) => path !== null))];
|
|
243
|
-
const renamedFrom =
|
|
244
|
-
const renamedTo =
|
|
247
|
+
const renamedFrom = /(?<=^|\n)rename from ([^\r\n]+)/.exec(section)?.[1];
|
|
248
|
+
const renamedTo = /(?<=^|\n)rename to ([^\r\n]+)/.exec(section)?.[1];
|
|
245
249
|
if (renamedFrom || renamedTo) {
|
|
246
250
|
return [...new Set([renamedFrom, renamedTo].filter((path) => path !== undefined).map(unquoteGitPath))];
|
|
247
251
|
}
|
|
248
|
-
const header =
|
|
252
|
+
const header = /(?<=^|\n)diff --git ([^\r\n]+)/.exec(section)?.[1];
|
|
249
253
|
return header ? gitHeaderPaths(header) : [];
|
|
250
254
|
}
|
|
251
255
|
function hunkPayload(body, sign) {
|
|
@@ -293,7 +297,7 @@ export function measureArtifactDiff(diff, manifest = CAPTURE_ARTIFACT_MANIFEST)
|
|
|
293
297
|
sections: [],
|
|
294
298
|
};
|
|
295
299
|
}
|
|
296
|
-
const rawSections = diff.split(
|
|
300
|
+
const rawSections = diff.split(DIFF_SECTIONS);
|
|
297
301
|
const kindOnly = kindOnlyPaths(rawSections);
|
|
298
302
|
const rendered = [];
|
|
299
303
|
const measurements = [];
|
|
@@ -321,7 +325,7 @@ export function measureArtifactDiff(diff, manifest = CAPTURE_ARTIFACT_MANIFEST)
|
|
|
321
325
|
const withheld = parsed.lines.slice(parsed.minus).join("\n");
|
|
322
326
|
const captureBytes = Buffer.byteLength(withheld, "utf8");
|
|
323
327
|
const receiptPath = paths.at(-1);
|
|
324
|
-
const receipt = `set aside: regenerable capture ${receiptPath} — ${captureBytes} bytes withheld (producer ${producer.producer}; provenance ${producer.provenance.source}#${producer.provenance.entrypoint}@${producer.provenance.revision})`;
|
|
328
|
+
const receipt = `set aside: regenerable capture ${quoteGitPath(receiptPath)} — ${captureBytes} bytes withheld (producer ${producer.producer}; provenance ${producer.provenance.source}#${producer.provenance.entrypoint}@${producer.provenance.revision})`;
|
|
325
329
|
const compact = `${parsed.lines.slice(0, parsed.minus).join("\n")}\n${receipt}\n`;
|
|
326
330
|
rendered.push(compact);
|
|
327
331
|
measurements.push({
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** A path exactly as git printed it, quoted or not, decoded to the name on disk. Unquoted input is returned as is. */
|
|
2
2
|
export declare function unquoteGitPath(raw: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* Queue rows 128/129: the inverse of unquoteGitPath, for a name tickmarkr writes back into patch text it parses again.
|
|
5
|
+
* A name holding a control character, U+2028/U+2029, a double quote or a backslash is C-quoted (those characters
|
|
6
|
+
* escaped, every other character kept), so no name can end a line; any other name is returned as is.
|
|
7
|
+
*/
|
|
8
|
+
export declare function quoteGitPath(path: string): string;
|
|
3
9
|
/**
|
|
4
10
|
* One side of a `--- ` / `+++ ` line, after its four-character marker: null for /dev/null, else the path without its
|
|
5
11
|
* a/ or b/ prefix. git ends a side holding a space with ONE tab (quoted or not, measured on git 2.54) — exactly that tab
|
package/dist/gates/git-paths.js
CHANGED
|
@@ -34,6 +34,28 @@ export function unquoteGitPath(raw) {
|
|
|
34
34
|
}
|
|
35
35
|
return Buffer.from(bytes).toString("utf8");
|
|
36
36
|
}
|
|
37
|
+
const NAMED = Object.fromEntries(Object.entries(ESCAPES).map(([name, code]) => [code, name]));
|
|
38
|
+
/**
|
|
39
|
+
* Queue rows 128/129: the inverse of unquoteGitPath, for a name tickmarkr writes back into patch text it parses again.
|
|
40
|
+
* A name holding a control character, U+2028/U+2029, a double quote or a backslash is C-quoted (those characters
|
|
41
|
+
* escaped, every other character kept), so no name can end a line; any other name is returned as is.
|
|
42
|
+
*/
|
|
43
|
+
export function quoteGitPath(path) {
|
|
44
|
+
if (!/[\x00-\x1f\x7f"\\\u2028\u2029]/.test(path))
|
|
45
|
+
return path;
|
|
46
|
+
let quoted = "";
|
|
47
|
+
for (const ch of path) {
|
|
48
|
+
const code = ch.codePointAt(0);
|
|
49
|
+
if (NAMED[code] !== undefined)
|
|
50
|
+
quoted += `\\${NAMED[code]}`;
|
|
51
|
+
else if (code < 0x20 || code === 0x7f || code === 0x2028 || code === 0x2029) {
|
|
52
|
+
quoted += [...Buffer.from(ch, "utf8")].map((byte) => `\\${byte.toString(8).padStart(3, "0")}`).join("");
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
quoted += ch;
|
|
56
|
+
}
|
|
57
|
+
return `"${quoted}"`;
|
|
58
|
+
}
|
|
37
59
|
/**
|
|
38
60
|
* One side of a `--- ` / `+++ ` line, after its four-character marker: null for /dev/null, else the path without its
|
|
39
61
|
* a/ or b/ prefix. git ends a side holding a space with ONE tab (quoted or not, measured on git 2.54) — exactly that tab
|
|
@@ -104,7 +104,7 @@ export function readProcessIdentity(pid, host = {}) {
|
|
|
104
104
|
const ps = spawnSync("ps", ["-ww", "-o", "pgid=", "-o", "stat=", "-o", "lstart=", "-o", "command=", "-p", String(pid)], {
|
|
105
105
|
encoding: "utf8", env: { ...process.env, LC_ALL: "C" },
|
|
106
106
|
});
|
|
107
|
-
const row = /^\s*(\d+)\s+(\S+)\s+(\w{3} \w{3} [ \d]\d \d\d:\d\d:\d\d \d{4})\s+(.*\S)\s
|
|
107
|
+
const row = /^\s*(\d+)\s+(\S+)\s+(\w{3} \w{3} [ \d]\d \d\d:\d\d:\d\d \d{4})\s+(.*\S)\s*$/s.exec(ps.stdout ?? "");
|
|
108
108
|
if (ps.status !== 0 || !row)
|
|
109
109
|
return confirmedDead(pid) ? "DEAD" : "UNKNOWN";
|
|
110
110
|
if (row[2].startsWith("Z"))
|
package/dist/run/daemon.js
CHANGED
|
@@ -1244,7 +1244,7 @@ export async function observeWorkerProcessTree(marker, cwd) {
|
|
|
1244
1244
|
return "unmeasurable";
|
|
1245
1245
|
const rows = [];
|
|
1246
1246
|
for (const line of snapshot.stdout.split("\n")) {
|
|
1247
|
-
const match = /^\s*(\d+)\s+(\d+)\s+(.*)
|
|
1247
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/s.exec(line);
|
|
1248
1248
|
if (match)
|
|
1249
1249
|
rows.push({ pid: match[1], ppid: match[2], command: match[3] });
|
|
1250
1250
|
}
|
|
@@ -1272,7 +1272,7 @@ const pathAtOrBelow = (root, candidate) => {
|
|
|
1272
1272
|
export function countLiveSuites(snapshot, repoRoot, daemonPid, cwdForPid, suiteParentForPid) {
|
|
1273
1273
|
const rows = [];
|
|
1274
1274
|
for (const line of snapshot.split("\n")) {
|
|
1275
|
-
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)
|
|
1275
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/s.exec(line);
|
|
1276
1276
|
if (match && !match[3].startsWith("Z")) {
|
|
1277
1277
|
rows.push({ pid: Number(match[1]), ppid: Number(match[2]), command: match[4] });
|
|
1278
1278
|
}
|
|
@@ -14,7 +14,7 @@ import { basename } from "node:path";
|
|
|
14
14
|
export const LAUNCHSERVICES_PROBE_CEILING_MS = 5_000;
|
|
15
15
|
// lstart in the C locale: "Wed Sep 30 21:54:33 2026"; comm is the executable path on Darwin.
|
|
16
16
|
const PS_ARGS = ["-A", "-o", "pid=,lstart=,comm="];
|
|
17
|
-
const PS_LINE = /^\s*(\d+)\s+(\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+?)\s
|
|
17
|
+
const PS_LINE = /^\s*(\d+)\s+(\w{3}\s+\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}\s+\d{4})\s+(.+?)\s*$/s;
|
|
18
18
|
const MAX_PS_BYTES = 4 * 1024 * 1024;
|
|
19
19
|
/** Exactly one launchservicesd row, or null (absent, ambiguous or malformed output is unknown). */
|
|
20
20
|
export function parsePsIdentity(stdout) {
|
package/dist/run/lease.js
CHANGED
|
@@ -330,7 +330,7 @@ export async function describeLeaseBlockers(pids) {
|
|
|
330
330
|
done("");
|
|
331
331
|
}
|
|
332
332
|
});
|
|
333
|
-
const executables = new Map(rows.split("\n").flatMap((line) => { const row = /^\s*(\d+)\s+(.*)
|
|
333
|
+
const executables = new Map(rows.split("\n").flatMap((line) => { const row = /^\s*(\d+)\s+(.*)$/s.exec(line); return row ? [[Number(row[1]), row[2].trim()]] : []; }));
|
|
334
334
|
const cwds = await import("./suite-census.js").then(({ batchedProcessCwds }) => batchedProcessCwds(pids)).catch(() => undefined);
|
|
335
335
|
return pids.map((pid) => {
|
|
336
336
|
const cwd = cwds?.get(pid);
|
|
@@ -363,7 +363,7 @@ export async function protectedProcesses(holder, tokenEnv = REPOSITORY_LEASE_TOK
|
|
|
363
363
|
return !leader || sameProcessBirth(processBirth(pid), holder.rootBirths?.[pid]);
|
|
364
364
|
}));
|
|
365
365
|
const parsed = lines.flatMap(line => {
|
|
366
|
-
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)
|
|
366
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/s.exec(line);
|
|
367
367
|
return match ? [{ pid: Number(match[1]), parent: Number(match[2]), group: Number(match[3]), stat: match[4], command: match[5] }] : [];
|
|
368
368
|
});
|
|
369
369
|
const parents = new Map(parsed.map(row => [row.pid, row.parent]));
|
package/dist/run/stall.js
CHANGED
|
@@ -118,7 +118,8 @@ export const workerReapHost = {
|
|
|
118
118
|
if (!lines.length || lines.length > 20_000)
|
|
119
119
|
return undefined;
|
|
120
120
|
for (const line of lines) {
|
|
121
|
-
|
|
121
|
+
// Queue row 127: the s flag — ps prints an argv's U+2028/U+2029 raw, and `.` alone never matches them.
|
|
122
|
+
const m = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+\s+\S+\s+\d+\s+\S+\s+\d+)\s+(.*)$/s.exec(line);
|
|
122
123
|
if (!m)
|
|
123
124
|
return undefined;
|
|
124
125
|
if (m[5].startsWith("Z"))
|
|
@@ -418,7 +419,7 @@ async function workerTreeCpuSnapshot(marker, cwd, group, identities) {
|
|
|
418
419
|
continue;
|
|
419
420
|
if (group !== undefined && Number(ids[3]) === group)
|
|
420
421
|
grouped.push(ids[1]);
|
|
421
|
-
const m = /^\s*(\d+)\s+(\d+)\s+\d+\s+(\S+)\s+(.*)
|
|
422
|
+
const m = /^\s*(\d+)\s+(\d+)\s+\d+\s+(\S+)\s+(.*)$/s.exec(line);
|
|
422
423
|
if (!m)
|
|
423
424
|
continue;
|
|
424
425
|
const cpu = parsePsCpu(m[3]);
|
package/dist/run/suite-census.js
CHANGED
|
@@ -56,7 +56,7 @@ export async function batchedProcessCwds(pids, exec = probeExec, proc = procRead
|
|
|
56
56
|
export function runnerPids(snapshot) {
|
|
57
57
|
const pids = [];
|
|
58
58
|
for (const line of snapshot.split("\n")) {
|
|
59
|
-
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)
|
|
59
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\S+)\s+(.*)$/s.exec(line);
|
|
60
60
|
if (match && !match[3].startsWith("Z") && isRunnerCommand(match[4]))
|
|
61
61
|
pids.push(Number(match[1]));
|
|
62
62
|
}
|
|
@@ -95,7 +95,7 @@ export async function batchedProcessProbes(pids, exec = probeExec, proc = procRe
|
|
|
95
95
|
if (rest.length === 0)
|
|
96
96
|
return;
|
|
97
97
|
for (const line of (await ask("ps", ["eww", "-p", rest.join(","), "-o", "pid=,command="])).split("\n")) {
|
|
98
|
-
const row = /^\s*(\d+)\s+(.*)
|
|
98
|
+
const row = /^\s*(\d+)\s+(.*)$/s.exec(line);
|
|
99
99
|
const value = row ? parentIn(row[2]) : undefined;
|
|
100
100
|
if (row && value)
|
|
101
101
|
parents.set(Number(row[1]), Number(value));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tickmarkr",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.8",
|
|
4
4
|
"description": "Spec in, verified work out.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -94,6 +94,7 @@
|
|
|
94
94
|
"skills/tickmarkr-auto/",
|
|
95
95
|
"skills/tickmarkr-loop/",
|
|
96
96
|
"skills/tickmarkr-overseer/",
|
|
97
|
+
"scripts/ci/",
|
|
97
98
|
"src/",
|
|
98
99
|
"tests/"
|
|
99
100
|
]
|
|
@@ -1601,9 +1601,12 @@ Create specialist seats using `orca terminal create --worktree path:<repo> --com
|
|
|
1601
1601
|
|
|
1602
1602
|
- **GRADE PUBLIC CI FROM JOB LOGS AT RUN-END ONLY (law 48).** A badge is not the grade, and
|
|
1603
1603
|
`gh run view --job --log` is empty while the run is in progress. At run-end execute
|
|
1604
|
-
`scripts/grade-ci.sh <run-id> <expected-count> <tag>` and require GREEN from
|
|
1605
|
-
|
|
1606
|
-
`
|
|
1604
|
+
`scripts/grade-ci.sh <run-id> <expected-count> <tag>` and require GREEN from every job of the
|
|
1605
|
+
declared inventory it prints first — serial `test` and `test-macos`, or sharded, per OS, the four
|
|
1606
|
+
`suite <os> k/4` shards, `single-fork <os>` and `aggregate <os>`; a missing, duplicated or unknown
|
|
1607
|
+
job reads UNREADABLE, never a skip. Its UNREADABLE state is a hard stop, not a green default.
|
|
1608
|
+
Derive the expected glob pathspec count with `git ls-files ':(glob)tests/**/*.test.ts'`
|
|
1609
|
+
(including the `.tsx` sibling where present), never a bare
|
|
1607
1610
|
`tests/**/*.test.ts`: bare `**` is not a git glob (law 49).
|
|
1608
1611
|
|
|
1609
1612
|
- **RE-READ THE REGISTRY BEFORE RULING (law 51).** A successful Release run and the first
|
|
@@ -1,9 +1,36 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# grade-ci.sh <run-id> <expected-count> [tag] — grade
|
|
3
|
-
# Grade only at run-end. A missing/in-progress/empty log is UNREADABLE, never evidence of green.
|
|
2
|
+
# grade-ci.sh <run-id> <expected-count> [tag] — grade EVERY job of a public CI run's closed inventory from
|
|
3
|
+
# its JOB LOG. Grade only at run-end. A missing/in-progress/empty log is UNREADABLE, never evidence of green.
|
|
4
4
|
# Exit 0 GREEN, 1 RED, 2 UNREADABLE, 3 INFRA_KILLED: a job the GitHub runner shut down or cancelled with
|
|
5
5
|
# no failed or timed-out test (the classifier's class, D-897) — never green, never a pass, only unproven.
|
|
6
6
|
# UNREADABLE dominates a mixed result, then RED: a failure anywhere dominates a kill.
|
|
7
|
+
#
|
|
8
|
+
# Two CLOSED inventories (v2.7.8 T4; the workflow renders these names): serial {test, test-macos}; sharded,
|
|
9
|
+
# for os in ubuntu-latest and macos-latest, `suite <os> k/4` for k 1..4, `single-fork <os>`, `aggregate <os>`.
|
|
10
|
+
# A run holding any sharded name is graded against the sharded inventory, else the serial one. The first
|
|
11
|
+
# line is `INVENTORY form=<serial|sharded> declared=<n> observed=<n>`; each missing, duplicated or unknown
|
|
12
|
+
# name prints `INVENTORY_DEFECT kind=<missing|duplicate|unknown> job=<json-quoted name>` and makes the run
|
|
13
|
+
# UNREADABLE: a job the inventory does not name is never skipped.
|
|
14
|
+
#
|
|
15
|
+
# A verdict line is read only from the log lines whose gh step column equals its declared step, exactly
|
|
16
|
+
# once, else UNREADABLE — a line a test or any other step prints, before or after the real one, never decides:
|
|
17
|
+
# COUNT_ORACLE <GREEN|RED> expected=<n|UNREADABLE> actual=<n|UNREADABLE> step `Assert collected test-file count`
|
|
18
|
+
# COVERAGE_FLOORS verdict=… floors=… failing=… (T1's final floors line) step `Recheck coverage floors`
|
|
19
|
+
# SET_ORACLE verdict=… expected=… actual=… defects=… (T3's set oracle) step `Assert collected test-file set`
|
|
20
|
+
# ROLES — the shared classifier (classify-vitest-log.sh) stays the one log verdict:
|
|
21
|
+
# serial the base rule (count oracle GREEN at the expected count, the classifier given that count
|
|
22
|
+
# reading CLEAN or RPC_ONLY, passed+skipped equal to it) AND exactly one COVERAGE_FLOORS
|
|
23
|
+
# verdict=GREEN, so a dropped floors step reads UNREADABLE.
|
|
24
|
+
# shard, single-fork
|
|
25
|
+
# GREEN only on conclusion success whose whole log the classifier, given no count, reads CLEAN
|
|
26
|
+
# or RPC_ONLY: their wrapper already applied the shard contract in-job.
|
|
27
|
+
# aggregate GREEN only on conclusion success with exactly one COVERAGE_FLOORS verdict=GREEN, exactly one
|
|
28
|
+
# `SET_ORACLE verdict=GREEN expected=N actual=N defects=0`, and the lines of its step
|
|
29
|
+
# `Merge coverage (one threshold check per OS)` classified CLEAN or RPC_ONLY.
|
|
30
|
+
# Exit annotations: SERIAL keeps the one exception run-ci-vitest.sh grants (a failure explained ONLY by steps
|
|
31
|
+
# that exited exactly 1 on a log of their own the classifier reads RPC_ONLY); any other failing annotation is
|
|
32
|
+
# never GREEN. Shard, single-fork and aggregate admit none — their wrapper applied the exception in-job.
|
|
33
|
+
# INFRA_KILLED is the base rule for every role.
|
|
7
34
|
set -u
|
|
8
35
|
|
|
9
36
|
run=${1:?run id required}
|
|
@@ -14,34 +41,96 @@ repo=${TKR_GRADE_CI_REPO:-alzahrani-khalid/tickmarkr}
|
|
|
14
41
|
out_dir=${TKR_GRADE_CI_DIR:-${TKR_STATE_DIR:-.tickmarkr}/overseer/diag}
|
|
15
42
|
mkdir -p "$out_dir" || { echo "UNREADABLE: cannot create log directory $out_dir"; exit 2; }
|
|
16
43
|
|
|
44
|
+
serial_inventory=$'test\ntest-macos'
|
|
45
|
+
sharded_inventory=$(for os in ubuntu-latest macos-latest; do
|
|
46
|
+
for k in 1 2 3 4; do echo "suite $os $k/4"; done
|
|
47
|
+
echo "single-fork $os"
|
|
48
|
+
echo "aggregate $os"
|
|
49
|
+
done)
|
|
50
|
+
COUNT_STEP="Assert collected test-file count"
|
|
51
|
+
FLOORS_STEP="Recheck coverage floors"
|
|
52
|
+
SET_STEP="Assert collected test-file set"
|
|
53
|
+
MERGE_STEP="Merge coverage (one threshold check per OS)"
|
|
54
|
+
COUNT_FORMAT='COUNT_ORACLE (GREEN|RED) expected=([0-9]+|UNREADABLE) actual=([0-9]+|UNREADABLE)'
|
|
55
|
+
FLOORS_FORMAT='COVERAGE_FLOORS verdict=(GREEN|RED|UNREADABLE) floors=([0-9]+|UNREADABLE) failing=([0-9]+|UNREADABLE)'
|
|
56
|
+
SET_FORMAT='SET_ORACLE verdict=(GREEN|RED|UNREADABLE) expected=([0-9]+|UNREADABLE) actual=([0-9]+|UNREADABLE) defects=[0-9]+'
|
|
57
|
+
|
|
17
58
|
verdict=0
|
|
18
59
|
mark_unreadable() { verdict=2; }
|
|
19
60
|
mark_red() { [ "$verdict" -eq 2 ] || verdict=1; }
|
|
20
61
|
mark_killed() { [ "$verdict" -ne 0 ] || verdict=3; }
|
|
21
62
|
field() { printf '%s\n' "$classified" | sed -n "s/^VITEST_LOG.* $1=\([^ ]*\).*/\1/p"; }
|
|
63
|
+
count() { printf '%s' "$1" | grep -c ''; }
|
|
64
|
+
# json <name> — one JSON string; awk -v decodes gh's @tsv escapes (\t \n \r \\) first
|
|
65
|
+
json() {
|
|
66
|
+
LC_ALL=C awk -v s="$1" 'BEGIN {
|
|
67
|
+
for (i = 1; i < 32; i++) esc[sprintf("%c", i)] = sprintf("\\u%04x", i)
|
|
68
|
+
esc["\""] = "\\\""; esc["\\"] = "\\\\"
|
|
69
|
+
for (i = 1; i <= length(s); i++) { c = substr(s, i, 1); out = out (c in esc ? esc[c] : c) }
|
|
70
|
+
print "\"" out "\""
|
|
71
|
+
}'
|
|
72
|
+
}
|
|
73
|
+
defect() { echo "INVENTORY_DEFECT kind=$1 job=$(json "$2")"; mark_unreadable; }
|
|
74
|
+
# step_log <log> <step> — the job-log lines gh's step column attributes to <step>, as gh printed them
|
|
75
|
+
step_log() { awk -F'\t' -v s="$2" 'NF >= 3 && $2 == s' "$1"; }
|
|
76
|
+
# verdict_line <log> <step> <family> <format> — the ONE <family> line <step> printed, in <format>;
|
|
77
|
+
# else MISSING, MULTIPLE(n) or MALFORMED, each UNREADABLE
|
|
78
|
+
verdict_line() {
|
|
79
|
+
local lines
|
|
80
|
+
lines=$(step_log "$1" "$2" | awk -v esc="$(printf '\033')" '{
|
|
81
|
+
gsub(/\^\[\[[0-9;]*m/, ""); gsub(esc "\\[[0-9;]*m", "")
|
|
82
|
+
sub(/^[^\t]*\t[^\t]*\t[0-9T:.-]+Z ?/, ""); sub(/[ \t\r]+$/, ""); print
|
|
83
|
+
}' | grep -E "^$3( |\$)")
|
|
84
|
+
case $(count "$lines") in
|
|
85
|
+
0) echo MISSING ;;
|
|
86
|
+
1) if printf '%s\n' "$lines" | grep -qxE "$4"; then printf '%s\n' "$lines"; else echo MALFORMED; fi ;;
|
|
87
|
+
*) echo "MULTIPLE($(count "$lines"))" ;;
|
|
88
|
+
esac
|
|
89
|
+
}
|
|
90
|
+
readable() { case $1 in MISSING|MULTIPLE*|MALFORMED) return 1 ;; esac; }
|
|
91
|
+
floors_green() { printf '%s\n' "$1" | grep -qxE 'COVERAGE_FLOORS verdict=GREEN floors=[0-9]+ failing=0'; }
|
|
22
92
|
|
|
23
93
|
jobs=$(gh run view "$run" --repo "$repo" --json jobs \
|
|
24
94
|
--jq '.jobs[] | [.databaseId, .name, .status, (.conclusion // "")] | @tsv') \
|
|
25
95
|
|| { echo "UNREADABLE: job list"; exit 2; }
|
|
26
|
-
|
|
96
|
+
# an empty job list is an inventory like any other: serial, observed=0, every declared job missing
|
|
97
|
+
|
|
98
|
+
names=$(printf '%s\n' "$jobs" | cut -f2)
|
|
99
|
+
if printf '%s\n' "$names" | grep -qxF -f <(printf '%s\n' "$sharded_inventory"); then
|
|
100
|
+
form=sharded; declared=$sharded_inventory
|
|
101
|
+
else
|
|
102
|
+
form=serial; declared=$serial_inventory
|
|
103
|
+
fi
|
|
104
|
+
echo "INVENTORY form=$form declared=$(count "$declared") observed=$(count "$names")"
|
|
105
|
+
while IFS= read -r name; do
|
|
106
|
+
case $(printf '%s\n' "$names" | grep -cxF -- "$name") in
|
|
107
|
+
0) defect missing "$name" ;;
|
|
108
|
+
1) ;;
|
|
109
|
+
*) defect duplicate "$name" ;;
|
|
110
|
+
esac
|
|
111
|
+
done <<< "$declared"
|
|
112
|
+
while IFS= read -r name; do
|
|
113
|
+
printf '%s\n' "$declared" | grep -qxF -- "$name" || defect unknown "$name"
|
|
114
|
+
done < <([ -z "$jobs" ] || printf '%s\n' "$names" | awk '!seen[$0]++')
|
|
27
115
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
116
|
+
[ -z "$jobs" ] || while IFS=$'\t' read -r id name status conclusion; do
|
|
117
|
+
# an unknown job has no role to grade: its INVENTORY_DEFECT already made the run UNREADABLE
|
|
118
|
+
printf '%s\n' "$declared" | grep -qxF -- "$name" || continue
|
|
31
119
|
case "$name" in
|
|
32
|
-
test)
|
|
33
|
-
|
|
34
|
-
*)
|
|
120
|
+
test|test-macos) role=serial ;;
|
|
121
|
+
suite\ *) role=shard ;;
|
|
122
|
+
single-fork\ *) role=single-fork ;;
|
|
123
|
+
*) role=aggregate ;;
|
|
35
124
|
esac
|
|
36
125
|
|
|
37
|
-
echo "$name: status=$status conclusion=${conclusion:-none} job=$id"
|
|
126
|
+
echo "$name: role=$role status=$status conclusion=${conclusion:-none} job=$id"
|
|
38
127
|
if [ "$status" != "completed" ]; then
|
|
39
128
|
echo "$name: UNREADABLE (job has not reached run-end)"
|
|
40
129
|
mark_unreadable
|
|
41
130
|
continue
|
|
42
131
|
fi
|
|
43
132
|
|
|
44
|
-
log="$out_dir/CI-$tag-$name.log"
|
|
133
|
+
log="$out_dir/CI-$tag-$(printf '%s' "$name" | tr -c 'A-Za-z0-9._-' '-').log"
|
|
45
134
|
gh run view --repo "$repo" --job "$id" --log > "$log" 2>/dev/null
|
|
46
135
|
if [ ! -s "$log" ]; then
|
|
47
136
|
echo "$name: UNREADABLE (empty log)"
|
|
@@ -49,24 +138,28 @@ while IFS=$'\t' read -r id name status conclusion; do
|
|
|
49
138
|
continue
|
|
50
139
|
fi
|
|
51
140
|
|
|
52
|
-
oracle=$(grep -oE 'COUNT_ORACLE [A-Z]+ expected=[0-9A-Z]+ actual=[0-9A-Z]+' "$log" | tail -1)
|
|
53
141
|
files=$(grep -oE 'Test Files .*' "$log" | sed 's/[[:space:]]*$//')
|
|
54
142
|
# OBS-1184: the log verdict comes from the ONE classifier the public CI wrapper also uses
|
|
55
143
|
# (scripts/run-ci-vitest.sh), so the badge and this grade cannot disagree. Its rules — complete
|
|
56
144
|
# summaries, failed/timed-out tests, coverage-threshold misses, and the RPC-timeout-only exception
|
|
57
145
|
# to "any unhandled error is RED" (OBS-1058) — live in classify-vitest-log.sh, stated once there.
|
|
58
|
-
#
|
|
59
|
-
|
|
146
|
+
# A serial job's expected count is its declared file contract: both steps' summaries must collect it all.
|
|
147
|
+
# A shard or single-fork job carries no count: its wrapper held it to its plan in-job.
|
|
148
|
+
if [ "$role" = serial ]; then
|
|
149
|
+
classified=$(bash "$here/classify-vitest-log.sh" "$log" "$expected" 2>&1)
|
|
150
|
+
else
|
|
151
|
+
classified=$(bash "$here/classify-vitest-log.sh" "$log" 2>&1)
|
|
152
|
+
fi
|
|
60
153
|
log_verdict=$(field verdict)
|
|
61
154
|
passed=$(field passed); skipped=$(field skipped); failed=$(field failed); timedout=$(field timedout)
|
|
62
155
|
unhandled=$(field unhandled); rpc=$(field runner_rpc_timeouts); coverage=$(field coverage_misses)
|
|
63
156
|
kills=$(field runner_kills)
|
|
64
157
|
errors=$(grep -oE '##\[error\].*' "$log" | sort | uniq -c | sed 's/^ *//' | tr '\n' ';')
|
|
65
158
|
# The commands' own outcomes stand, as in the wrapper: GREEN needs a success conclusion with no step
|
|
66
|
-
# exit annotation, or a failure explained ONLY by steps that exited exactly 1 on a log
|
|
67
|
-
# (gh's step column) that the classifier calls RPC_ONLY
|
|
68
|
-
#
|
|
69
|
-
#
|
|
159
|
+
# exit annotation, or — SERIAL only — a failure explained ONLY by steps that exited exactly 1 on a log
|
|
160
|
+
# of their own (gh's step column) that the classifier calls RPC_ONLY. Any other code (2, a signal's
|
|
161
|
+
# 137), any annotation on another role, an unexplained failure, or a cancelled, timed-out or missing
|
|
162
|
+
# conclusion is RED.
|
|
70
163
|
exits=$(grep -E '##\[error\]Process completed with exit code [0-9]+' "$log")
|
|
71
164
|
case "$conclusion" in
|
|
72
165
|
success) outcome=ok ;;
|
|
@@ -76,9 +169,9 @@ while IFS=$'\t' read -r id name status conclusion; do
|
|
|
76
169
|
if [ -n "$exits" ]; then
|
|
77
170
|
while IFS= read -r exit_line; do
|
|
78
171
|
code=$(printf '%s\n' "$exit_line" | sed -E 's/.*Process completed with exit code ([0-9]+).*/\1/')
|
|
79
|
-
if [ "$code" != 1 ]; then outcome="exit-$code"; break; fi
|
|
172
|
+
if [ "$code" != 1 ] || [ "$role" != serial ]; then outcome="exit-$code"; break; fi
|
|
80
173
|
step=$(printf '%s\n' "$exit_line" | awk -F'\t' 'NF >= 3 { print $2 }')
|
|
81
|
-
|
|
174
|
+
step_log "$log" "$step" > "$log.step"
|
|
82
175
|
case $(bash "$here/classify-vitest-log.sh" "$log.step" 2>&1) in
|
|
83
176
|
"VITEST_LOG verdict=RPC_ONLY "*) ;;
|
|
84
177
|
*) outcome="exit-1-not-rpc-only"; break ;;
|
|
@@ -86,36 +179,60 @@ while IFS=$'\t' read -r id name status conclusion; do
|
|
|
86
179
|
done <<< "$exits"
|
|
87
180
|
fi
|
|
88
181
|
|
|
89
|
-
|
|
90
|
-
|
|
182
|
+
oracles=
|
|
183
|
+
case $role in
|
|
184
|
+
serial)
|
|
185
|
+
oracle=$(verdict_line "$log" "$COUNT_STEP" COUNT_ORACLE "$COUNT_FORMAT")
|
|
186
|
+
floors=$(verdict_line "$log" "$FLOORS_STEP" COVERAGE_FLOORS "$FLOORS_FORMAT")
|
|
187
|
+
oracles="oracle=[$oracle] floors=[$floors] " ;;
|
|
188
|
+
aggregate)
|
|
189
|
+
floors=$(verdict_line "$log" "$FLOORS_STEP" COVERAGE_FLOORS "$FLOORS_FORMAT")
|
|
190
|
+
set_oracle=$(verdict_line "$log" "$SET_STEP" SET_ORACLE "$SET_FORMAT")
|
|
191
|
+
step_log "$log" "$MERGE_STEP" > "$log.merge"
|
|
192
|
+
merge=$(bash "$here/classify-vitest-log.sh" "$log.merge" 2>&1 | sed -n 's/^VITEST_LOG verdict=\([^ ]*\) reason=\([^ ]*\) .*/\1 \2/p')
|
|
193
|
+
oracles="floors=[$floors] set=[$set_oracle] merge=[$merge] " ;;
|
|
194
|
+
esac
|
|
195
|
+
|
|
196
|
+
echo "$name: ${oracles}files=[$(printf '%s' "$files" | tr '\n' '|')] passed=$passed skipped=$skipped failed=$failed timedout=$timedout unhandled=$unhandled runner_rpc_timeouts=$rpc coverage_misses=$coverage runner_kills=${kills:-?} log=[$(field reason)] outcome=[$outcome] errors=[$errors]"
|
|
197
|
+
# A killed runner skips the oracle steps, so a missing verdict line is the kill's, not an unreadable log:
|
|
91
198
|
# INFRA_KILLED unless a step's own failing exit or a failed/timed-out test dominates it (then RED).
|
|
92
199
|
if [ "$log_verdict" = INFRA_KILLED ] && [ "${outcome#exit-}" = "$outcome" ]; then
|
|
93
200
|
echo "$name: INFRA_KILLED"
|
|
94
201
|
mark_killed
|
|
202
|
+
continue
|
|
95
203
|
elif [ "${kills:-0}" -gt 0 ] 2>/dev/null; then
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
echo "$name: UNREADABLE"
|
|
100
|
-
mark_unreadable
|
|
101
|
-
elif [ "$oracle" = "COUNT_ORACLE GREEN expected=$expected actual=$expected" ] \
|
|
102
|
-
&& { [ "$log_verdict" = CLEAN ] || [ "$log_verdict" = RPC_ONLY ]; } && [ "$outcome" = ok ] \
|
|
103
|
-
&& [ $((passed + skipped)) -eq "$expected" ]; then
|
|
104
|
-
echo "$name: GREEN"
|
|
204
|
+
grade=RED
|
|
205
|
+
elif [ -z "$log_verdict" ]; then
|
|
206
|
+
grade=UNREADABLE
|
|
105
207
|
else
|
|
106
|
-
|
|
107
|
-
|
|
208
|
+
case $log_verdict in CLEAN|RPC_ONLY) log_ok=1 ;; *) log_ok= ;; esac
|
|
209
|
+
grade=RED
|
|
210
|
+
case $role in
|
|
211
|
+
serial)
|
|
212
|
+
if ! readable "$oracle" || ! readable "$floors" || [ -z "$files" ]; then
|
|
213
|
+
grade=UNREADABLE
|
|
214
|
+
elif [ "$oracle" = "COUNT_ORACLE GREEN expected=$expected actual=$expected" ] && floors_green "$floors" \
|
|
215
|
+
&& [ -n "$log_ok" ] && [ "$outcome" = ok ] && [ $((passed + skipped)) -eq "$expected" ]; then
|
|
216
|
+
grade=GREEN
|
|
217
|
+
fi ;;
|
|
218
|
+
shard|single-fork)
|
|
219
|
+
if [ "$outcome" = ok ] && [ "$conclusion" = success ] && [ -n "$log_ok" ]; then grade=GREEN; fi ;;
|
|
220
|
+
aggregate)
|
|
221
|
+
if ! readable "$floors" || ! readable "$set_oracle"; then
|
|
222
|
+
grade=UNREADABLE
|
|
223
|
+
elif [ "$outcome" = ok ] && [ "$conclusion" = success ] && floors_green "$floors" \
|
|
224
|
+
&& [ "$set_oracle" = "SET_ORACLE verdict=GREEN expected=$expected actual=$expected defects=0" ] \
|
|
225
|
+
&& { [ "${merge%% *}" = CLEAN ] || [ "${merge%% *}" = RPC_ONLY ]; }; then
|
|
226
|
+
grade=GREEN
|
|
227
|
+
fi ;;
|
|
228
|
+
esac
|
|
108
229
|
fi
|
|
230
|
+
echo "$name: $grade"
|
|
231
|
+
case $grade in
|
|
232
|
+
RED) mark_red ;;
|
|
233
|
+
UNREADABLE) mark_unreadable ;;
|
|
234
|
+
esac
|
|
109
235
|
done <<< "$jobs"
|
|
110
236
|
|
|
111
|
-
if [ "$seen_test" -ne 1 ]; then
|
|
112
|
-
echo "test: UNREADABLE (job missing from run)"
|
|
113
|
-
mark_unreadable
|
|
114
|
-
fi
|
|
115
|
-
if [ "$seen_macos" -ne 1 ]; then
|
|
116
|
-
echo "test-macos: UNREADABLE (job missing from run)"
|
|
117
|
-
mark_unreadable
|
|
118
|
-
fi
|
|
119
|
-
|
|
120
237
|
echo "VERDICT rc=$verdict (0=all GREEN 1=RED 2=UNREADABLE 3=INFRA_KILLED)"
|
|
121
238
|
exit "$verdict"
|