tickmarkr 1.84.0 → 1.85.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/dist/adapters/claude-code.d.ts +1 -0
- package/dist/adapters/claude-code.js +57 -1
- package/dist/adapters/fake.js +9 -0
- package/dist/adapters/types.d.ts +3 -0
- package/dist/adapters/types.js +21 -0
- package/dist/cli/commands/status.js +160 -30
- package/dist/compile/collateral.d.ts +86 -2
- package/dist/compile/collateral.js +294 -3
- package/dist/config/config.d.ts +62 -0
- package/dist/config/config.js +157 -2
- package/dist/drivers/herdr.d.ts +20 -3
- package/dist/drivers/herdr.js +288 -105
- package/dist/gates/baseline.d.ts +1 -0
- package/dist/gates/baseline.js +91 -13
- package/dist/gates/review.d.ts +7 -0
- package/dist/gates/review.js +99 -6
- package/dist/gates/run-gates.d.ts +9 -0
- package/dist/gates/run-gates.js +285 -41
- package/dist/run/daemon.d.ts +48 -2
- package/dist/run/daemon.js +1417 -315
- package/dist/run/journal.d.ts +56 -3
- package/dist/run/journal.js +275 -1
- package/dist/run/stall.d.ts +35 -1
- package/dist/run/stall.js +118 -8
- package/dist/tui/cockpit/components.d.ts +2 -0
- package/dist/tui/cockpit/derive.d.ts +29 -2
- package/dist/tui/cockpit/derive.js +219 -23
- package/dist/tui/cockpit/run-cockpit.js +128 -27
- package/package.json +1 -1
package/dist/gates/baseline.js
CHANGED
|
@@ -14,16 +14,76 @@ const PASS_LINE_RE = /^\s*(?:(?:[\w@./-]+:\s*)*[✓✔]|(?:\[tickmarkr\]\s+)?(?:
|
|
|
14
14
|
// output to headline it. \s is fine in a TS regex — the BSD [[:space:]] rule binds shell grep only.
|
|
15
15
|
// OBS-42: vitest's diagnostic failure headings are shared anchors for baseline and tip verification.
|
|
16
16
|
const FAIL_ANCHOR_RE = /^\s*(?:FAIL\s+|[^\w]*(?:Unhandled Errors|Uncaught Exception)\b)/;
|
|
17
|
-
|
|
17
|
+
// OBS-278: a failure fingerprint is a SHAPE — the way a runner reports a failure — never error/fail
|
|
18
|
+
// vocabulary. A status surface that draws those words (zone +N, run attempt N, "1 failed") used to
|
|
19
|
+
// fingerprint as a fresh failure on every attempt, rejecting a task for rendering what it was chartered
|
|
20
|
+
// to render. Digits are written (?:\d+|#) so a shape matches both raw and digit-normalized lines.
|
|
21
|
+
// Run summaries: " Tests N failed | M passed (T)" (vitest), "# fail N" (TAP / node:test),
|
|
22
|
+
// "ℹ fail N" (node:test's spec reporter) and "test result: FAILED. …" (cargo / libtest).
|
|
23
|
+
const SUMMARY_FAIL_RE = /^\s*(?:Tests?\s+(?:Files?\s+)?(?:\d+|#)\s+failed|#\s+fail\s+(?!0\b)(?:\d+|#)\b|ℹ\s+fail\s+(?!0\b)(?:\d+|#)\b|test result:\s+FAILED\b)/;
|
|
24
|
+
const ERROR_ANCHOR_RE = /^\s*(?:Error|[A-Za-z_$][\w$]*Error):\s+\S/;
|
|
25
|
+
const TSC_ERROR_RE = /^\s*\S.*\((?:\d+|#),(?:\d+|#)\):\s+error\s+[A-Z]+(?:\d+|#):/i; // tsc
|
|
26
|
+
const LINTER_ERROR_RE = /^\s*(?:\d+|#):(?:\d+|#)\s+error\s+\S/; // eslint stylish
|
|
27
|
+
// The failure shapes non-Vitest runners name their tests with: pytest's short summary
|
|
28
|
+
// (`FAILED tests/t.py::test_x - AssertionError`, `ERROR tests/t.py::fixture`), go test
|
|
29
|
+
// (`--- FAIL: TestFoo (0.00s)`), TAP / node:test (`not ok 1 - name`, whose summary line lands in
|
|
30
|
+
// SUMMARY_FAIL_RE) and python unittest's failure-block header (`FAIL: test_x (mod.Class.test_x)`,
|
|
31
|
+
// verbatim capture, python3 -m unittest -v). Each anchors the runner's token at line START; the
|
|
32
|
+
// pytest/go forms also require a file/test identifier after it (`::`, a path separator, or an
|
|
33
|
+
// extension) — so a surface drawing "FAILED" mid-line, or a plain sentence like "FAILED to reach
|
|
34
|
+
// the zone", still matches nothing. Without these a genuinely new pytest/go/TAP/unittest failure on
|
|
35
|
+
// an already-red baseline was forgiven as pre-existing, because only shaped lines can reject.
|
|
36
|
+
const RUNNER_FAIL_RE = /^\s*(?:(?:FAILED|ERROR)\s+\S*(?:::|[/\\]|\.[A-Za-z]\w*\b)|FAIL:\s+\S|---\s+FAIL:\s+\S|not ok\b)/;
|
|
37
|
+
// The other half of the position rule: runners that put the verdict LAST. cargo/libtest names each
|
|
38
|
+
// failing test as `test tests::name ... FAILED` (verbatim capture, cargo 1.95.0) and details it with
|
|
39
|
+
// `thread 'tests::name' (81651804) panicked at src/lib.rs:7:24:`; python unittest -v writes
|
|
40
|
+
// `test_x (mod.Class.test_x) ... FAIL` (and `... ERROR`) — the same rule with the runner's
|
|
41
|
+
// qualified-name token between identifier and separator, and the short verdict spelling (verbatim
|
|
42
|
+
// capture, python3 -m unittest -v). Recognition is positional, not a vendor name: an identifier, the
|
|
43
|
+
// runner's own separator, then the verdict ENDING the line. A drawn status strip ("│ ✗ tip-verify
|
|
44
|
+
// FAILED · zone +3 │") has the verdict mid-line inside chrome, so it matches nothing here — which is
|
|
45
|
+
// why this can generalize without re-opening OBS-278.
|
|
46
|
+
const TRAILING_FAIL_RE = /^\s*(?:test\s+)?\S+(?:\s+\([^)]*\))?\s+(?:\.{3}|-{3,})\s+(?:FAIL(?:ED)?|ERROR)\s*$|^\s*thread\s+'[^']*'\s+.*panicked at\s+\S/;
|
|
47
|
+
// node:test's spec reporter speaks glyphs, not words: a failure is named by a leading ✖ (verbatim
|
|
48
|
+
// capture, Node v22 `--test-reporter=spec`: `✖ old failure (0.585875ms)`, totalled as `ℹ fail 1` in
|
|
49
|
+
// SUMMARY_FAIL_RE) — the exact counterpart of the ✓/✔ pass markers PASS_LINE_RE drops, with the same
|
|
50
|
+
// optional label prefixes. Recognition is positional — the runner's own marker at line start — so any
|
|
51
|
+
// runner sharing the glyph protocol is read without being enumerated; a status strip drawing ✖
|
|
52
|
+
// mid-line inside chrome matches nothing, the same position rule as the other shapes.
|
|
53
|
+
const GLYPH_FAIL_RE = /^\s*(?:(?:[\w@./-]+:\s*)*)✖\s+\S/;
|
|
54
|
+
// Lines that NAME a failing test — the ones worth headlining to the operator. One list, so recognition
|
|
55
|
+
// and reporting cannot drift apart (a shape that blocks but never gets named cost 3 attempts once).
|
|
56
|
+
const namesFailure = (l) => FAIL_ANCHOR_RE.test(l) || RUNNER_FAIL_RE.test(l) || TRAILING_FAIL_RE.test(l) || GLYPH_FAIL_RE.test(l);
|
|
57
|
+
const isFailureShaped = (l) => namesFailure(l) || SUMMARY_FAIL_RE.test(l) || ERROR_ANCHOR_RE.test(l)
|
|
58
|
+
|| TSC_ERROR_RE.test(l) || LINTER_ERROR_RE.test(l);
|
|
59
|
+
const VOCAB_RE = /\b(?:error|fail(?:ed|ure|ing)?)\b/i;
|
|
18
60
|
const normalizeLine = (l) => l.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
|
|
61
|
+
// A failing command whose output holds no shape any runner here names. The marker is content-free and
|
|
62
|
+
// constant: downstream consumers (tip verify journals fingerprint counts) still see that the command
|
|
63
|
+
// failed, while no line of the output — narration, status strip, stack trace — can enter the set. Being
|
|
64
|
+
// constant it is identical on every attempt, so it can never surface as a fresh fingerprint either.
|
|
65
|
+
export const UNRECOGNIZED_FAILURE = "<unrecognized failure output>";
|
|
66
|
+
// OBS-278 dies here: only a failure SHAPE is fingerprintable. Vocabulary is not a shape — a surface that
|
|
67
|
+
// renders "zone +3 · run attempt 2 · 1 failed" (what T9 is chartered to draw) contributes nothing, with
|
|
68
|
+
// or without box glyphs, so it cannot manufacture a fresh-fingerprint rejection on any future attempt.
|
|
19
69
|
export function fingerprint(output) {
|
|
20
70
|
const lines = output
|
|
21
71
|
.split("\n")
|
|
22
72
|
.map((l) => l.replace(ANSI_RE, ""))
|
|
23
|
-
.filter((l) => !PASS_LINE_RE.test(l)
|
|
24
|
-
|
|
25
|
-
|
|
73
|
+
.filter((l) => !PASS_LINE_RE.test(l));
|
|
74
|
+
const shaped = lines.filter(isFailureShaped);
|
|
75
|
+
if (!shaped.length)
|
|
76
|
+
return lines.some((l) => l.trim()) ? [UNRECOGNIZED_FAILURE] : [];
|
|
77
|
+
return [...new Set(shaped.map(normalizeLine))];
|
|
26
78
|
}
|
|
79
|
+
// The diagnostic channel for a runner we cannot read: raw output lines, never fingerprints, never a
|
|
80
|
+
// verdict. They exist so the operator sees SOMETHING when a green command turns red unrecognizably.
|
|
81
|
+
const unrecognizedEvidence = (raw) => raw
|
|
82
|
+
.split("\n")
|
|
83
|
+
.map((l) => l.replace(ANSI_RE, "").trimEnd())
|
|
84
|
+
.filter((l) => VOCAB_RE.test(l))
|
|
85
|
+
.slice(0, 10)
|
|
86
|
+
.join("\n");
|
|
27
87
|
// stored baselines may predate ANSI/pass-marker hardening — renormalize at compare time so existing
|
|
28
88
|
// on-disk baseline.json files stay comparable without recapture (compat invariant, CLAUDE.md)
|
|
29
89
|
const renormalize = (fp) => normalizeLine(fp.replace(ANSI_RE, ""));
|
|
@@ -68,7 +128,9 @@ export async function captureBaseline(cwd, commands) {
|
|
|
68
128
|
// ponytail: strip the executing cwd so repo-root capture and worktree compare fingerprint identically; /private-vs-/tmp symlink variance is out of scope
|
|
69
129
|
base.commands[name] = {
|
|
70
130
|
exitCode: r.code,
|
|
71
|
-
|
|
131
|
+
// a command that exits 0 has no failures to fingerprint — recording any would be a lie the
|
|
132
|
+
// compare step then has to forgive
|
|
133
|
+
fingerprints: r.code === 0 ? [] : fingerprint((r.stdout + "\n" + r.stderr).split(cwd).join("")),
|
|
72
134
|
missingCommand: missingConfiguredCommand(cmd, r),
|
|
73
135
|
};
|
|
74
136
|
}
|
|
@@ -119,12 +181,12 @@ function headlineDetails(raw, fresh) {
|
|
|
119
181
|
const headlines = raw
|
|
120
182
|
.split("\n")
|
|
121
183
|
.map((l) => l.replace(ANSI_RE, ""))
|
|
122
|
-
.filter((l) =>
|
|
184
|
+
.filter((l) => namesFailure(l) || SUMMARY_FAIL_RE.test(l));
|
|
123
185
|
if (!headlines.length)
|
|
124
186
|
return { details: `new failures vs baseline:\n${fresh.join("\n")}` };
|
|
125
187
|
return {
|
|
126
188
|
details: `failing tests:\n${headlines.join("\n")}\n\nnew failure fingerprints vs baseline (secondary):\n${fresh.join("\n")}`,
|
|
127
|
-
meta: { failingTests: headlines.filter(
|
|
189
|
+
meta: { failingTests: headlines.filter(namesFailure) },
|
|
128
190
|
};
|
|
129
191
|
}
|
|
130
192
|
export async function compareToBaseline(cwd, commands, baseline, enabled) {
|
|
@@ -145,18 +207,34 @@ export async function compareToBaseline(cwd, commands, baseline, enabled) {
|
|
|
145
207
|
const raw = (r.stdout + "\n" + r.stderr).split(cwd).join("");
|
|
146
208
|
const known = new Set((baseline.commands[name]?.fingerprints ?? []).map(renormalize));
|
|
147
209
|
// OBS-42: diagnostic headings enrich fingerprints but cannot invalidate legacy baselines.
|
|
148
|
-
const
|
|
149
|
-
|
|
210
|
+
const current = fingerprint(raw);
|
|
211
|
+
const fresh = current.filter((f) => !known.has(f) && (!FAIL_ANCHOR_RE.test(f) || f.startsWith("FAIL ")));
|
|
212
|
+
// OBS-278: only a failure SHAPE is a verdict — everything fingerprint() keeps is one, except the
|
|
213
|
+
// unrecognized-output marker, which is evidence for the operator and never grounds to reject.
|
|
214
|
+
// ponytail: ceiling — a runner whose failure output holds no shape above and whose baseline is
|
|
215
|
+
// already red has its new failures forgiven, so forgiveness that rests on the marker SAYS so
|
|
216
|
+
// below rather than reading as a verified green. Raise the ceiling by teaching isFailureShaped
|
|
217
|
+
// that runner's position rule (leading verdict + identifier, or identifier + separator + trailing
|
|
218
|
+
// verdict); loosening back to vocabulary re-opens OBS-278.
|
|
219
|
+
const unreadable = current.includes(UNRECOGNIZED_FAILURE);
|
|
220
|
+
const failing = fresh.filter((f) => f !== UNRECOGNIZED_FAILURE);
|
|
221
|
+
if (!failing.length && (baseline.commands[name]?.exitCode ?? 1) === 0) {
|
|
222
|
+
const closed = `command was green at baseline but now exits ${r.code} with no recognizable failure lines — failing closed`;
|
|
223
|
+
const evidence = unrecognizedEvidence(raw);
|
|
150
224
|
results.push({
|
|
151
225
|
gate: name,
|
|
152
226
|
pass: false,
|
|
153
|
-
details:
|
|
227
|
+
details: evidence ? `${closed}\nunrecognized output:\n${evidence}` : closed,
|
|
154
228
|
});
|
|
155
229
|
continue;
|
|
156
230
|
}
|
|
157
|
-
results.push(
|
|
158
|
-
? { gate: name, pass: false, ...headlineDetails(raw,
|
|
159
|
-
: {
|
|
231
|
+
results.push(failing.length
|
|
232
|
+
? { gate: name, pass: false, ...headlineDetails(raw, failing) }
|
|
233
|
+
: {
|
|
234
|
+
gate: name,
|
|
235
|
+
pass: true,
|
|
236
|
+
details: `exit ${r.code} but only pre-existing failures (forgiven)${unreadable ? " — no failure shape recognized in this output, so a new failure from this runner is invisible to the baseline gate" : ""}`,
|
|
237
|
+
});
|
|
160
238
|
}
|
|
161
239
|
return results;
|
|
162
240
|
}
|
package/dist/gates/review.d.ts
CHANGED
|
@@ -27,6 +27,13 @@ export declare function isProtectedEvidence(path: string): boolean;
|
|
|
27
27
|
export declare function setAsideReceiptPath(section: string): string | null;
|
|
28
28
|
/** Replace the content of every section confined to the regenerable frame corpora with a receipt. */
|
|
29
29
|
export declare function setAsideRegenerableCaptures(diff: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* The paths this task's diff ACTUALLY touched. `-z` so a path carrying spaces or non-ASCII bytes is
|
|
32
|
+
* never mangled by git's quoting, `--no-renames` so a rename reports BOTH sides: a file renamed OUT of
|
|
33
|
+
* the leaf class must be visible to the promotion test, and rename detection would hide the old side.
|
|
34
|
+
*/
|
|
35
|
+
export declare function changedPaths(worktree: string, baseRef: string): Promise<string[]>;
|
|
36
|
+
export declare function mirrorsVersionOnly(worktree: string, baseRef: string, path: string): Promise<boolean>;
|
|
30
37
|
export declare function fetchTaskDiff(worktree: string, baseRef: string): Promise<{
|
|
31
38
|
full: string;
|
|
32
39
|
forCap: string;
|
package/dist/gates/review.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import { channelKey } from "../adapters/types.js";
|
|
4
|
-
import { DEFAULT_DIFF_CAP, TIER_RANK } from "../config/config.js";
|
|
3
|
+
import { channelKey, shq } from "../adapters/types.js";
|
|
4
|
+
import { criticalPathHits, DEFAULT_DIFF_CAP, DEFAULT_REVIEW_CRITICAL_PATHS, declaredReviewPolicy, isReviewLeafPath, raiseReviewPolicy, REVIEW_VERSION_MIRRORS, TIER_RANK, } from "../config/config.js";
|
|
5
5
|
import { renderAcceptanceItem } from "../graph/schema.js";
|
|
6
6
|
import { getAdapter } from "../adapters/registry.js";
|
|
7
7
|
import { shOk } from "../run/git.js";
|
|
@@ -211,6 +211,34 @@ export function setAsideRegenerableCaptures(diff) {
|
|
|
211
211
|
const kindOnly = kindOnlyPaths(sections);
|
|
212
212
|
return sections.map((s) => setAsideSection(s, kindOnly)).join("");
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* The paths this task's diff ACTUALLY touched. `-z` so a path carrying spaces or non-ASCII bytes is
|
|
216
|
+
* never mangled by git's quoting, `--no-renames` so a rename reports BOTH sides: a file renamed OUT of
|
|
217
|
+
* the leaf class must be visible to the promotion test, and rename detection would hide the old side.
|
|
218
|
+
*/
|
|
219
|
+
export async function changedPaths(worktree, baseRef) {
|
|
220
|
+
const out = await shOk(`git diff --name-only --no-renames -z '${baseRef}..HEAD'`, worktree);
|
|
221
|
+
return [...new Set(out.split("\0").map((p) => p.trim()).filter(Boolean))].sort();
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* A root manifest is a version MIRROR only when the bump is all it changed. `package.json` carries the
|
|
225
|
+
* gate commands and the dependency set, so a diff that moves `scripts` or `dependencies` is executable
|
|
226
|
+
* behaviour wearing a leaf-class path — the one thing a path predicate can never see for itself. Every
|
|
227
|
+
* added or removed line must be a `"version":` line (a lockfile bump rewrites several of them); a
|
|
228
|
+
* manifest whose diff cannot be read at all fails closed, out of the class.
|
|
229
|
+
*/
|
|
230
|
+
const VERSION_FIELD_LINE_RE = /^[+-]\s*"version":\s*"[^"]*",?\s*$/;
|
|
231
|
+
export async function mirrorsVersionOnly(worktree, baseRef, path) {
|
|
232
|
+
let diff;
|
|
233
|
+
try {
|
|
234
|
+
diff = await shOk(`git diff -U0 '${baseRef}..HEAD' -- ${shq(path)}`, worktree);
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
const changed = diff.split("\n").filter((l) => /^[+-]/.test(l) && !/^(?:\+\+\+|---)/.test(l));
|
|
240
|
+
return changed.length > 0 && changed.every((l) => VERSION_FIELD_LINE_RE.test(l));
|
|
241
|
+
}
|
|
214
242
|
export async function fetchTaskDiff(worktree, baseRef) {
|
|
215
243
|
const full = setAsideRegenerableCaptures(await shOk(`git diff '${baseRef}..HEAD'`, worktree));
|
|
216
244
|
const forCap = setAsideRegenerableCaptures(await shOk(`git diff -U0 '${baseRef}..HEAD'`, worktree));
|
|
@@ -269,9 +297,74 @@ export async function reviewGate(task, worktree, baseRef, author, channels, adap
|
|
|
269
297
|
// OBS-196: run dir for raw-output persistence on an unparseable verdict; absent (older callers,
|
|
270
298
|
// direct tests) skips persistence and changes nothing else.
|
|
271
299
|
artifactDir) {
|
|
272
|
-
|
|
273
|
-
|
|
300
|
+
// R3 (OBS-186): participation is keyed on PATHS. The compiler's assignment comes from the DECLARED
|
|
301
|
+
// files[]; the operator's floor may RAISE it to full and can never lower it. `complexityThreshold` is
|
|
302
|
+
// retired — the branch that returned a green skip on a complexity comparison is gone, and with it the
|
|
303
|
+
// "a law caps complexity at 3, the gate starts at 7" unreachability OBS-186 measured.
|
|
304
|
+
//
|
|
305
|
+
// COLLATERAL this rescoped task closed: the run-gates/daemon participation assertions are rewritten
|
|
306
|
+
// path-keyed, the NamedFake review fixtures carry their own nonce trailer (llm.ts's injection is
|
|
307
|
+
// scoped to adapter id "fake" and a renamed fake is a different adapter to it — the check is not
|
|
308
|
+
// weakened, the fixture is fixed), the merge decision reads `gateSatisfied`, and the daemon writes a
|
|
309
|
+
// parallel round's gate-result rows in GATE_NAMES order (src/run/daemon.ts).
|
|
310
|
+
//
|
|
311
|
+
// That last one is why: retiring the switch makes fixtures that used to SKIP review journal TWO
|
|
312
|
+
// verdict rows per round instead of one, and judge ‖ review publish in COMPLETION order — so the
|
|
313
|
+
// three journal-determinism oracles (tests/run/narration.test.ts's byte comparison and its
|
|
314
|
+
// throwing-sink event-order check, tests/run/notify-identity.test.ts's two-run equality, and this
|
|
315
|
+
// repo's own phase-start/gate-result pairing in tests/run/daemon.test.ts) start seeing a race.
|
|
316
|
+
// LATENT, not introduced: the operator's config has run `complexityThreshold: 0` since 2026-07-31,
|
|
317
|
+
// so production rounds have journaled both siblings all along — only the fixtures were blind to it.
|
|
318
|
+
// Fixed in the ledger rather than in the oracles, because determinism run-to-run is a property of
|
|
319
|
+
// the journal, not of three test files that happen to assert it.
|
|
320
|
+
const declaredPolicy = declaredReviewPolicy(task.files);
|
|
321
|
+
const policy = raiseReviewPolicy(declaredPolicy, cfg.review.policy);
|
|
322
|
+
// PROMOTION: the declared assignment is a claim about paths, and the diff is the evidence. A
|
|
323
|
+
// judge-only task whose diff left the leaf class is reviewed in full — the claim never outranks
|
|
324
|
+
// what actually happened, and an empty diff promotes too (a skip earned by an absence is not earned).
|
|
325
|
+
let promotedBy = null;
|
|
326
|
+
if (policy === "judge-only") {
|
|
327
|
+
const touched = await changedPaths(worktree, baseRef);
|
|
328
|
+
// Two ways a path leaves the leaf class. It is not a member (`docs/tool.ts`, `docs/Makefile`) — or
|
|
329
|
+
// it is a root version mirror whose diff moved more than the version field, which no path predicate
|
|
330
|
+
// can see. `package.json` carries the gate commands, so a scripts edit hiding behind a leaf-class
|
|
331
|
+
// path is exactly the promotion this buys.
|
|
332
|
+
const nonMembers = touched.filter((p) => !isReviewLeafPath(p));
|
|
333
|
+
const impostorMirrors = (await Promise.all(touched.filter((p) => REVIEW_VERSION_MIRRORS.has(p))
|
|
334
|
+
.map(async (p) => await mirrorsVersionOnly(worktree, baseRef, p) ? null : p))).filter((p) => p !== null);
|
|
335
|
+
// The fail-closed backstop for the compile lint. reviewParticipationErrors runs at a repo root the
|
|
336
|
+
// compile seam cannot always name (collateral.ts); THIS gate is handed the run's real config, so a
|
|
337
|
+
// critical path that reached dispatch is reviewed here whatever the lint saw. The shipped defaults
|
|
338
|
+
// are unioned in for the same reason they are there: a config that names none still has a floor.
|
|
339
|
+
const criticalHits = criticalPathHits([...task.files, ...touched], [...new Set([...DEFAULT_REVIEW_CRITICAL_PATHS, ...(cfg.review.criticalPaths ?? [])])]);
|
|
340
|
+
const escaped = [...new Set([...nonMembers, ...impostorMirrors, ...criticalHits])].sort();
|
|
341
|
+
if (touched.length > 0 && escaped.length === 0) {
|
|
342
|
+
// A declined review makes NO green claim. types.ts's T11 note ("pass stays true so enforcement is
|
|
343
|
+
// unchanged") describes the baseline skips — a build/test/lint command the repo never configured.
|
|
344
|
+
// R3 overrules it here: a cross-vendor review that did not run cannot report a pass, so the record
|
|
345
|
+
// carries verdict "skipped" with the policy that declined it and the reason, and pass is never
|
|
346
|
+
// true. The merge-predicate seam this opens is named in the collateral note above.
|
|
347
|
+
return {
|
|
348
|
+
gate: "review",
|
|
349
|
+
pass: false,
|
|
350
|
+
details: `skipped — reviewPolicy judge-only: every declared path is docs/CHANGELOG/RELEASING/version-mirror leaf work and the diff stayed in that class (${touched.join(", ")})`,
|
|
351
|
+
meta: {
|
|
352
|
+
skipped: true,
|
|
353
|
+
verdict: "skipped",
|
|
354
|
+
policy: "judge-only",
|
|
355
|
+
reason: "every declared path and every path this diff touched is provably leaf-class work",
|
|
356
|
+
paths: touched,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
promotedBy = escaped.length > 0 ? escaped : [];
|
|
274
361
|
}
|
|
362
|
+
// Every verdict this gate reports from here on was produced under `full` — either declared full, or
|
|
363
|
+
// promoted here. `promotedBy` names the paths that bought the promotion, so the record shows WHY.
|
|
364
|
+
const policyMeta = {
|
|
365
|
+
policy: "full",
|
|
366
|
+
...(promotedBy ? { promotedFrom: declaredPolicy, promotedBy } : {}),
|
|
367
|
+
};
|
|
275
368
|
const reviewer = pickReviewer(author, channels, excludeReviewers ?? [], cfg.review.prefer ?? []);
|
|
276
369
|
if (!reviewer) {
|
|
277
370
|
// meta.noEligibleReviewer lets run-gates' review-retry keep the ORIGINAL unparseable result when
|
|
@@ -343,7 +436,7 @@ The top-level comments array is optional. Use it only for actionable line-anchor
|
|
|
343
436
|
gate: "review",
|
|
344
437
|
pass: false,
|
|
345
438
|
details: `review output unparseable (reviewer ${reviewer.adapter}:${reviewer.model}; cause: ${cause}${saved ? `; raw saved: ${saved}` : ""}) — failing closed`,
|
|
346
|
-
meta: { reviewer: channelKey(reviewer), unparseable: true, cause },
|
|
439
|
+
meta: { ...policyMeta, reviewer: channelKey(reviewer), unparseable: true, cause },
|
|
347
440
|
};
|
|
348
441
|
}
|
|
349
442
|
const decided = findings !== null
|
|
@@ -354,6 +447,6 @@ The top-level comments array is optional. Use it only for actionable line-anchor
|
|
|
354
447
|
gate: "review",
|
|
355
448
|
pass: decided.pass,
|
|
356
449
|
details: appendAnchoredReview(prose, v),
|
|
357
|
-
meta: { reviewer: channelKey(reviewer) },
|
|
450
|
+
meta: { ...policyMeta, reviewer: channelKey(reviewer) },
|
|
358
451
|
};
|
|
359
452
|
}
|
|
@@ -9,6 +9,7 @@ export type GateEvent = {
|
|
|
9
9
|
gate: GateName;
|
|
10
10
|
index: number;
|
|
11
11
|
total: number;
|
|
12
|
+
parentAt?: number;
|
|
12
13
|
} | {
|
|
13
14
|
phase: "end";
|
|
14
15
|
gate: GateName;
|
|
@@ -27,8 +28,16 @@ export interface GateContext {
|
|
|
27
28
|
via?: GateVia;
|
|
28
29
|
excludeReviewers?: string[];
|
|
29
30
|
artifactDir?: string;
|
|
31
|
+
pipeline?: "v185" | "legacy";
|
|
32
|
+
selectTests?: boolean;
|
|
30
33
|
onGate?: (e: GateEvent) => void | Promise<void>;
|
|
31
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* The configured test command narrowed to these files. Mirrors testFiltered's `--` rule (acceptance.ts:104):
|
|
37
|
+
* npm/yarn/pnpm/npx script wrappers need one `--` to forward positional filters to the underlying runner;
|
|
38
|
+
* a command that already has `--` takes them directly. Every path is quoted — config flows into a shell.
|
|
39
|
+
*/
|
|
40
|
+
export declare function testCommandForFiles(testCmd: string, files: string[]): string;
|
|
32
41
|
export declare function runGates(task: Task, ctx: GateContext): Promise<{
|
|
33
42
|
results: GateResult[];
|
|
34
43
|
commits: string[];
|