Skip to content

Commit e396cd0

Browse files
claude[bot]claude
andauthored
fix(pm): render a ci-failure fix as the one remedy it is (#10620)
`fix` arrives from `classifyTransportProbe` as the wrapped lines of a single sentence, but both printers in `scripts/pm/ci-failure.mjs` prefixed every element with `fix:`, so one remedy rendered as three and a reader counting "how many things do I have to do" counted wrong. Prefix the first line only and pad the continuations under it — the idiom `check-half-states.mjs`, the file that PRODUCES these verdicts, already prints with. Both call sites now go through one helper so they cannot drift apart again. The lines are padded rather than joined and re-wrapped: the `bad-credential-anon-reachable` remedy is a copy-pasteable command followed by an annotation that carries its own indentation and an arrow pointing back at the command, and re-flowing would swallow the command into the prose. Presentation only — no exit code, no verdict, no classification changes. Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5e5df6a commit e396cd0

1 file changed

Lines changed: 80 additions & 3 deletions

File tree

scripts/pm/ci-failure.mjs

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2264,6 +2264,51 @@ async function selfTest() {
22642264
['repo-scope-refused', ['rate:anon', 'repo']],
22652265
);
22662266

2267+
// -- renderFixLines: one remedy is one `fix:` marker (#10362) -------------
2268+
// Driven through the REAL producer, not a hand-written array: what needs
2269+
// pinning is that this file renders what `classifyTransportProbe` actually
2270+
// emits. A fixture would only restate the printer's own assumption about the
2271+
// shape, which is the assumption that was wrong.
2272+
const refusedFix = classifyTransportProbe({
2273+
token: 'ghp_x',
2274+
authed: { status: 200, rateLimitRemaining: 4999 },
2275+
repo: { status: 403, rateLimitRemaining: 4999 },
2276+
}).fix;
2277+
t('the producer still wraps this remedy across several lines', refusedFix.length > 1, true);
2278+
t(
2279+
'a multi-line remedy renders with exactly ONE fix: marker',
2280+
renderFixLines(refusedFix).filter((l) => l.startsWith(' fix: ')).length,
2281+
1,
2282+
);
2283+
t(
2284+
'its continuations are padded under the marker rather than re-marked',
2285+
renderFixLines(refusedFix).slice(1).every((l) => l.startsWith(' ') && !l.includes('fix: ')),
2286+
true,
2287+
);
2288+
t('rendering neither adds a line nor drops one', renderFixLines(refusedFix).length, refusedFix.length);
2289+
2290+
// The branch that makes re-wrapping unsafe: a copy-pasteable command whose
2291+
// annotation carries its own indentation and an arrow pointing back at it.
2292+
const anonFix = classifyTransportProbe({
2293+
token: 'ghp_x',
2294+
authed: { status: 401 },
2295+
anon: { status: 200, rateLimitRemaining: 59 },
2296+
}).fix;
2297+
t(
2298+
'the copy-pasteable command keeps the marked line to itself',
2299+
renderFixLines(anonFix)[0],
2300+
' fix: GITHUB_TOKEN= GH_TOKEN= node scripts/pm/check-half-states.mjs',
2301+
);
2302+
t(
2303+
'the annotation keeps its own indent, so its arrow still points at that command',
2304+
renderFixLines(anonFix)[1].startsWith(' ↑'),
2305+
true,
2306+
);
2307+
2308+
t('an empty fix renders nothing at all', renderFixLines([]), []);
2309+
t('an absent fix renders nothing at all', renderFixLines(undefined), []);
2310+
t('a one-line remedy is just the marked line', renderFixLines(['do the thing']), [' fix: do the thing']);
2311+
22672312
if (failures.length > 0) {
22682313
console.error(`✗ ci-failure --self-test (${failures.length} failure(s)):\n`);
22692314
for (const f of failures) console.error(` • ${f}`);
@@ -2295,10 +2340,42 @@ async function selfTest() {
22952340
' annotations carried none, a log that anchored nothing is still a shortfall rather than a\n' +
22962341
' manufactured answer, a 410 is named as expired retention rather than as an absence of\n' +
22972342
' evidence, a CONNECT refusal lands as transport with the blocked host to report, and a\n' +
2298-
' tail with no `##[error]` in it is labelled a window rather than an anchor.',
2343+
' tail with no `##[error]` in it is labelled a window rather than an anchor. And a `fix`\n' +
2344+
' renders as the ONE remedy it is: a single `fix:` marker with its continuations padded\n' +
2345+
' under it, keeping a copy-pasteable command on a line of its own.',
22992346
);
23002347
}
23012348

2349+
/**
2350+
* Render a verdict's `fix` as the ONE remedy it is: the marker goes on the first
2351+
* line, and every continuation is padded to sit under it (#10362).
2352+
*
2353+
* `fix` arrives as the wrapped lines of a single sentence — never as a list of
2354+
* independent remedies (measured across every branch of
2355+
* `classifyTransportProbe`). Marking each element `fix:` rendered one remedy as
2356+
* three, and a reader counting "how many things do I have to do" counted wrong.
2357+
*
2358+
* The lines are padded rather than re-flowed because the producer's breaks are
2359+
* load-bearing in at least one branch: `bad-credential-anon-reachable` is a
2360+
* copy-pasteable command followed by an annotation carrying its own
2361+
* indentation, whose `↑` points at the command above it. Joining and
2362+
* re-wrapping would swallow the command into the prose and leave the arrow
2363+
* pointing at nothing, so continuations keep their own leading whitespace.
2364+
*
2365+
* This is the idiom `check-half-states.mjs` — the file that PRODUCES these
2366+
* verdicts — already prints with. Both of this file's printers go through here
2367+
* so the two cannot drift apart again, which is how the second one came to
2368+
* exist (#10155 matched the entry point's spelling rather than fixing it in an
2369+
* unrelated PR).
2370+
*
2371+
* Presentation only: no exit code, no verdict, no claim about the tree.
2372+
*/
2373+
function renderFixLines(fix) {
2374+
const lines = fix ?? [];
2375+
if (lines.length === 0) return [];
2376+
return [` fix: ${lines[0]}`, ...lines.slice(1).map((l) => (l ? ` ${l}` : ''))];
2377+
}
2378+
23022379
/**
23032380
* The mid-walk net's printer (#10155). The DECISION is `midWalkVerdict`, pure
23042381
* and self-tested; what lives here is the one thing it cannot be handed — the
@@ -2320,7 +2397,7 @@ async function reportMidWalkFailure(error, stage) {
23202397
const decision = midWalkVerdict({ probe, error, read: error?.read ?? null, stage });
23212398
console.error(`\nci-failure: ${decision.verdict}${decision.headline}\n`);
23222399
for (const line of decision.detail) console.error(line ? ` ${line}` : '');
2323-
for (const line of decision.fix ?? []) console.error(` fix: ${line}`);
2400+
for (const line of renderFixLines(decision.fix)) console.error(line);
23242401
console.error(" Piping reports the PIPE's status, so `... | tail` reads green either way. Use `echo \"EXIT=$?\"`.");
23252402
process.exit(decision.exit);
23262403
}
@@ -2377,7 +2454,7 @@ if (!invokedDirectly) {
23772454
if (probe.kind !== 'reachable') {
23782455
console.error(`ci-failure: PREREQUISITE NOT MET — ${probe.headline}`);
23792456
for (const line of probe.detail ?? []) console.error(` ${line}`);
2380-
for (const line of probe.fix ?? []) console.error(` fix: ${line}`);
2457+
for (const line of renderFixLines(probe.fix)) console.error(line);
23812458
console.error(` (Exit ${EXIT_PREREQUISITE_NOT_MET}. This classifies the ENVIRONMENT, not the tree.)`);
23822459
process.exit(EXIT_PREREQUISITE_NOT_MET);
23832460
}

0 commit comments

Comments
 (0)