omp-conductor 0.15.9 → 0.15.11
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 +273 -2543
- package/REFERENCE.md +2638 -0
- package/package.json +3 -2
- package/schema/config.schema.json +8 -23
- package/src/arm-challenge.ts +112 -0
- package/src/ask.ts +434 -0
- package/src/board.ts +81 -15
- package/src/brief-upgrade.ts +114 -8
- package/src/briefs/orchestrator.md +55 -29
- package/src/briefs/policy.md +14 -5
- package/src/briefs/worker.md +7 -1
- package/src/chain-check.ts +1 -1
- package/src/check-trailing-newlines.ts +82 -0
- package/src/cli.ts +190 -1391
- package/src/commands/arm.ts +21 -0
- package/src/commands/board.ts +23 -0
- package/src/commands/brief-upgrade.ts +186 -0
- package/src/commands/context.ts +49 -0
- package/src/commands/daemon.ts +71 -0
- package/src/commands/dashboard.ts +74 -0
- package/src/commands/decision.ts +103 -0
- package/src/commands/disarm.ts +21 -0
- package/src/commands/doctor.ts +98 -0
- package/src/commands/event.ts +62 -0
- package/src/commands/extend.ts +64 -0
- package/src/commands/friction.ts +56 -0
- package/src/commands/help.ts +9 -0
- package/src/commands/hold.ts +26 -0
- package/src/commands/intake.ts +134 -0
- package/src/commands/ledger.ts +69 -0
- package/src/commands/message.ts +48 -0
- package/src/commands/report.ts +170 -0
- package/src/commands/restart.ts +76 -0
- package/src/commands/resume.ts +58 -0
- package/src/commands/setup.ts +93 -0
- package/src/commands/start.ts +23 -0
- package/src/commands/stats.ts +131 -0
- package/src/commands/status.ts +48 -0
- package/src/commands/stop.ts +51 -0
- package/src/commands/tail.ts +109 -0
- package/src/commands/unblock.ts +39 -0
- package/src/commands/upgrade-install.ts +31 -0
- package/src/commands/upgrade-rollback.ts +23 -0
- package/src/commands/upgrade.ts +25 -0
- package/src/commands/verb.ts +83 -0
- package/src/commands/version.ts +30 -0
- package/src/commands/worker.ts +100 -0
- package/src/config-schema.ts +38 -1
- package/src/config.ts +10 -3
- package/src/daemon.ts +613 -94
- package/src/dashboard/app.js +120 -0
- package/src/dashboard/index.html +34 -0
- package/src/dashboard/server.ts +267 -0
- package/src/dashboard/style.css +180 -0
- package/src/decisions.ts +39 -14
- package/src/diff-flags.ts +131 -241
- package/src/doctor.ts +795 -0
- package/src/escalate.ts +60 -19
- package/src/failure-class.ts +29 -3
- package/src/fleet.ts +58 -1
- package/src/graph-health.ts +1 -1
- package/src/label-projection.ts +1 -1
- package/src/lifecycle.ts +198 -2
- package/src/notices.ts +9 -0
- package/src/omp.ts +2 -0
- package/src/orchestrator-tick.ts +315 -17
- package/src/release-policy.ts +135 -23
- package/src/reports.ts +19 -5
- package/src/setup-host.ts +420 -8
- package/src/setup-install.ts +69 -14
- package/src/setup-wizard.ts +199 -61
- package/src/setup.ts +131 -35
- package/src/stats.ts +331 -0
- package/src/store.ts +206 -21
- package/src/tracker/github.ts +27 -4
- package/src/types.ts +144 -31
- package/src/unblock.ts +55 -11
- package/src/upgrade-journal.ts +220 -0
- package/src/upgrade-verify.ts +506 -0
- package/src/upgrade.ts +295 -26
- package/src/verbs/actions.ts +73 -1
- package/src/verbs/protocol.ts +29 -4
- package/src/verbs/server.ts +183 -20
- package/systemd/omp-conductor-recover.sh +433 -0
- package/systemd/omp-conductor.service.example +7 -0
- package/systemd/recover-unit-test.sh +428 -0
package/src/diff-flags.ts
CHANGED
|
@@ -2,19 +2,21 @@
|
|
|
2
2
|
* The settlement audit: checking a worker's own account of its work against the
|
|
3
3
|
* pull request it actually pushed.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The report's `changed:` line — the one fact a reviewer needs to use a
|
|
6
|
+
* settlement report as a summary — is no longer hand-written at all. It is
|
|
7
|
+
* derived at settlement from the pull request's own file list: {@link
|
|
8
|
+
* deriveChangedLine} composes it and {@link withDerivedChangedLine} rewrites it
|
|
9
|
+
* into the report, so the disclosure is the diff's, never a model's memory of
|
|
10
|
+
* work that happened dozens of turns earlier (#488). Only the *narrative* — what
|
|
11
|
+
* changed and why — remains the worker's to write.
|
|
7
12
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* module does the same job for `changed:`, and for the epic's ranked item 7,
|
|
16
|
-
* "don't weaken tests you didn't write, checked by diff review", where until now
|
|
17
|
-
* the reviewer was the worker reviewing itself (#128).
|
|
13
|
+
* What this module still checks by reading a report is content, not disclosure.
|
|
14
|
+
* `state: pushed-green` stopped being believed in #85 — a claim is now checked
|
|
15
|
+
* against GitHub by `verifyPushedGreenClaim` rather than taken at face value —
|
|
16
|
+
* and the epic's ranked item 7, "don't weaken tests you didn't write, checked
|
|
17
|
+
* by diff review", is the other half: a diff-level check of the test files a PR
|
|
18
|
+
* touched, where until now the reviewer was the worker reviewing itself
|
|
19
|
+
* (#128).
|
|
18
20
|
*
|
|
19
21
|
* Everything here is **advisory**. A flag never changes a run's state, never
|
|
20
22
|
* blocks a merge and never fails a run. The judgement a flag invites — "was
|
|
@@ -28,11 +30,11 @@
|
|
|
28
30
|
* being checked. So **every heuristic below resolves ambiguity towards
|
|
29
31
|
* silence**, and each one names the blind spot it accepts to stay quiet.
|
|
30
32
|
*
|
|
31
|
-
* Pure by construction: the analyser takes a parsed diff
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* Pure by construction: the analyser takes a parsed diff and the dispatching
|
|
34
|
+
* issue's text, and returns flags. Fetching the diff is the tracker's job
|
|
35
|
+
* (`Tracker.prDiff`) — the same split `prStateFrom`/`prState` already use — so
|
|
36
|
+
* every rule here is pinned by a recorded diff rather than by a live
|
|
37
|
+
* repository.
|
|
36
38
|
*/
|
|
37
39
|
|
|
38
40
|
import type { PrDiff, PrDiffFile, SettlementFlag } from "./types.ts";
|
|
@@ -231,51 +233,51 @@ function isTestPath(path: string): boolean {
|
|
|
231
233
|
return TEST_FILE_NAME.test(path) || TEST_DIR.test(path);
|
|
232
234
|
}
|
|
233
235
|
|
|
236
|
+
// --------------------------------------------------------- derived disclosure
|
|
237
|
+
|
|
234
238
|
/**
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
+
* The `changed:` line is composed from the PR's own diff — every file it
|
|
240
|
+
* touched, lockfiles included. Nothing is exempted, because nothing is being
|
|
241
|
+
* held against the worker any more: the list is the diff's, so completeness
|
|
242
|
+
* costs nothing.
|
|
239
243
|
*/
|
|
240
|
-
const DERIVED_FILE: Record<string, true> = {
|
|
241
|
-
"bun.lock": true,
|
|
242
|
-
"bun.lockb": true,
|
|
243
|
-
"package-lock.json": true,
|
|
244
|
-
"pnpm-lock.yaml": true,
|
|
245
|
-
"yarn.lock": true,
|
|
246
|
-
"Cargo.lock": true,
|
|
247
|
-
"poetry.lock": true,
|
|
248
|
-
"uv.lock": true,
|
|
249
|
-
"go.sum": true,
|
|
250
|
-
"Gemfile.lock": true,
|
|
251
|
-
"composer.lock": true,
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
// -------------------------------------------------------- `changed:` reconcile
|
|
255
244
|
|
|
245
|
+
/** The `changed:` line of the settlement report, as the rewrite targets it. */
|
|
256
246
|
const CHANGED_LINE = /^[ \t>]*changed:[ \t]*(.*)$/im;
|
|
257
247
|
|
|
258
248
|
/**
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
*
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
249
|
+
* Any other report field heading: `word:` opening a line, possibly indented or
|
|
250
|
+
* blockquote-prefixed (`> next:`). A `changed:` disclosure never continues
|
|
251
|
+
* past one — the rewrite below ends its old-list scan at a following field,
|
|
252
|
+
* so `next:`/`gates:` values stay attached to the report (#435).
|
|
253
|
+
*/
|
|
254
|
+
const FIELD_LINE = /^[ \t>]*[A-Za-z_][A-Za-z0-9_-]*\s*:/;
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* A token that reads as a path — the shape a hand-written file list is made
|
|
258
|
+
* of. The rewrite scans a wrapped old disclosure line by line and stops where
|
|
259
|
+
* the lines stop looking like paths: `omp/src/foo.ts` is a path, `(new)` and
|
|
260
|
+
* `e.g` are prose. The last clause is not fussiness: without it `e.g`, `i.e`
|
|
261
|
+
* and `0.3.18` would all count as paths and a wrapped list's boundary would
|
|
262
|
+
* land one line too late.
|
|
266
263
|
*/
|
|
267
264
|
const CLAIMED_PATH = /^(?:[\w.@~+-]+\/)+[\w.@~*+-]*$|^[\w.@~+-]*\.[A-Za-z][A-Za-z0-9]{1,7}$/;
|
|
268
265
|
|
|
269
|
-
/** A Python
|
|
270
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
* `i.e.`, `a.k.a`) stay commentary: they are exactly the prose the path
|
|
275
|
-
* heuristic exists to reject, and each one that slipped through would become
|
|
276
|
-
* an "you claimed a file you never touched" flag on an honest report. */
|
|
266
|
+
/** A Python module name: `services.rule_purge`, `omp.routing` — a legitimate
|
|
267
|
+
* way a report names a file by import path rather than location (#224), and
|
|
268
|
+
* still a path-shaped token for the old-list scan. Every segment is at least
|
|
269
|
+
* two characters so prose abbreviations (`e.g`, `i.e.`, `a.k.a`) stay
|
|
270
|
+
* commentary. */
|
|
277
271
|
const DOTTED_MODULE = /^[A-Za-z_]\w+(?:\.[A-Za-z_]\w+)+$/;
|
|
278
272
|
|
|
273
|
+
/** A numeric diff statistic — `+1/-1`, `+12/-3`, `1/-1`, a bare `+1`. It is
|
|
274
|
+
* slash-shaped enough to look like a path — every segment is a signed
|
|
275
|
+
* integer, which CLAIMED_PATH's character classes accept — but it is a count,
|
|
276
|
+
* not a path: the wrapped-list scan must cut at it, exactly where it appears
|
|
277
|
+
* in real reports (`omp/package.json (…, +1/-1; …)`, the #388 report,
|
|
278
|
+
* verbatim). */
|
|
279
|
+
const DIFF_STAT = /^[+-]?\d+(?:\/[+-]?\d+)?$/;
|
|
280
|
+
|
|
279
281
|
/** One-level brace expansion: `a/{b,c}.py` → [`a/b.py`, `a/c.py`]. Braces are
|
|
280
282
|
* how a report compresses adjacent files into one token; each comma-separated
|
|
281
283
|
* alternative is substituted into the skeleton. Nesting is deliberately
|
|
@@ -300,72 +302,71 @@ function expandExtensionAlternation(token: string): string[] {
|
|
|
300
302
|
return [first, ...rest.split("/.").filter(Boolean)].map((extension) => `${base}.${extension}`);
|
|
301
303
|
}
|
|
302
304
|
|
|
303
|
-
/**
|
|
304
|
-
*
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
for (const raw of line.split(/[\s;]+|,(?![^{}]*\})/)) {
|
|
312
|
-
const token = raw.replace(/^[`'"([*-]+/, "").replace(/[`'")\].,:;]+$/, "");
|
|
313
|
-
if (token === "" || token === "none") continue;
|
|
314
|
-
const normalised = token.replace(/^\.?\//, "");
|
|
315
|
-
// Expansion happens here so every downstream consumer — both `covers`
|
|
316
|
-
// directions — sees plain paths: a brace token dies earlier at the
|
|
317
|
-
// CLAIMED_PATH filter if it is never opened up (#224).
|
|
318
|
-
for (const expanded of expandBraces(normalised)) {
|
|
319
|
-
const alternatives = expandExtensionAlternation(expanded);
|
|
320
|
-
for (const path of alternatives) {
|
|
321
|
-
if (!CLAIMED_PATH.test(path) && !DOTTED_MODULE.test(path)) continue;
|
|
322
|
-
seen.add(path);
|
|
323
|
-
if (alternatives.length > 1) extensionAlternatives?.add(path);
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
return [...seen];
|
|
305
|
+
/** One cleaned token of a `changed:` line, as claim parsing sees it: leading
|
|
306
|
+
* quotes and brackets stripped, trailing punctuation stripped, empties, the
|
|
307
|
+
* word `none`, and numeric diff statistics dropped. */
|
|
308
|
+
function claimTokens(line: string): string[] {
|
|
309
|
+
return line
|
|
310
|
+
.split(/[\s;]+|,(?![^{}]*\})/)
|
|
311
|
+
.map((raw) => raw.replace(/^[`'"([*-]+/, "").replace(/[`'")\].,:;]+$/, ""))
|
|
312
|
+
.filter((token) => token !== "" && token !== "none" && !DIFF_STAT.test(token));
|
|
328
313
|
}
|
|
329
314
|
|
|
330
|
-
|
|
331
|
-
|
|
315
|
+
/** Whether a cleaned token names at least one path once braces and extension
|
|
316
|
+
* alternatives are expanded — the same filter the disclosure rewrite applies
|
|
317
|
+
* to every continuation line it considers part of the old file list. */
|
|
318
|
+
function tokenNamesAPath(token: string): boolean {
|
|
319
|
+
const normalised = token.replace(/^\.?\//, "");
|
|
320
|
+
return expandBraces(normalised).some((expanded) =>
|
|
321
|
+
expandExtensionAlternation(expanded).some((path) => CLAIMED_PATH.test(path) || DOTTED_MODULE.test(path)),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The file list the PR's own diff reports, in diff order, comma-separated —
|
|
327
|
+
* the settlement report's `changed:` line. Derived rather than asked for, so a
|
|
328
|
+
* worker that never writes a file list still settles with a correct one, and a
|
|
329
|
+
* report that contradicts the diff is rewritten to say what the diff says
|
|
330
|
+
* (never the other way round).
|
|
331
|
+
*/
|
|
332
|
+
export function deriveChangedLine(diff: PrDiff): string {
|
|
333
|
+
return diff.files.map((file) => file.path).join(", ");
|
|
332
334
|
}
|
|
333
335
|
|
|
334
336
|
/**
|
|
335
|
-
*
|
|
336
|
-
*
|
|
337
|
+
* Rewrite the worker's hand-written `changed:` line with the derived one,
|
|
338
|
+
* keeping the rest of the report — the narrative — verbatim.
|
|
337
339
|
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
342
|
-
* - `*` as a within-segment wildcard, because `omp/src/tracker/*` is how the
|
|
343
|
-
* issues in this repo themselves name a file set.
|
|
340
|
+
* The old line and any wrapped continuation that still reads as a path list
|
|
341
|
+
* (#435) are removed wholesale and replaced by a single derived line. A report
|
|
342
|
+
* with no `changed:` line at all has one inserted before `next:`, so a settled
|
|
343
|
+
* report always carries the disclosure.
|
|
344
344
|
*/
|
|
345
|
-
function
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
`^${claim.replaceAll(/[.*+?^${}()|[\]\\]/g, (ch) => (ch === "*" ? "[^/]*" : `\\${ch}`))}$`,
|
|
354
|
-
);
|
|
355
|
-
return pattern.test(path) || pattern.test(basename(path));
|
|
345
|
+
export function withDerivedChangedLine(report: string, changedLine: string): string {
|
|
346
|
+
const lines = report.split("\n");
|
|
347
|
+
const start = lines.findIndex((line) => CHANGED_LINE.test(line));
|
|
348
|
+
const disclosure = `changed: ${changedLine}`;
|
|
349
|
+
if (start === -1) {
|
|
350
|
+
const next = lines.findIndex((line) => /^[ \t>]*next:/.test(line));
|
|
351
|
+
lines.splice(next === -1 ? lines.length : next, 0, disclosure);
|
|
352
|
+
return lines.join("\n");
|
|
356
353
|
}
|
|
357
|
-
|
|
358
|
-
//
|
|
359
|
-
//
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
if (
|
|
354
|
+
// The old disclosure is the `changed:` line plus whatever following lines
|
|
355
|
+
// still read as a wrapped path list: indented, non-blank, not a field heading,
|
|
356
|
+
// not prose. Everything past it belongs to the report and survives.
|
|
357
|
+
let end = start + 1;
|
|
358
|
+
while (end < lines.length) {
|
|
359
|
+
const line = lines[end] ?? "";
|
|
360
|
+
if (line.trim() === "") break;
|
|
361
|
+
if (!/^[ \t>]/.test(line)) break;
|
|
362
|
+
if (FIELD_LINE.test(line)) break;
|
|
363
|
+
const tokens = claimTokens(line);
|
|
364
|
+
if (tokens.length === 0 || !tokenNamesAPath(tokens[0] ?? "")) break;
|
|
365
|
+
end++;
|
|
366
366
|
}
|
|
367
|
-
const
|
|
368
|
-
|
|
367
|
+
const prefix = /^[ \t>]*/.exec(lines[start] ?? "")?.[0] ?? "";
|
|
368
|
+
lines.splice(start, end - start, `${prefix}${disclosure}`);
|
|
369
|
+
return lines.join("\n");
|
|
369
370
|
}
|
|
370
371
|
|
|
371
372
|
// ------------------------------------------------------ test-weakening lexicon
|
|
@@ -492,143 +493,41 @@ function evidence(text: string): string {
|
|
|
492
493
|
// -------------------------------------------------------------------- analyser
|
|
493
494
|
|
|
494
495
|
export interface SettlementAudit {
|
|
495
|
-
/** The worker's final report, verbatim. */
|
|
496
|
-
report: string;
|
|
497
|
-
/** Earlier attempts' reports for the same issue, oldest first. Their
|
|
498
|
-
* `changed:` disclosures are pooled as coverage so a file disclosed in a
|
|
499
|
-
* prior attempt is never flagged as undisclosed in the final one, while the
|
|
500
|
-
* reverse (unmatched-claim) direction reads only the current report — a file
|
|
501
|
-
* claimed in attempt 1 and reverted by attempt 3 must not come back as a new
|
|
502
|
-
* finding (#199). Absent means this is the only attempt, or older rows never
|
|
503
|
-
* stored a report. */
|
|
504
|
-
priorReports?: readonly string[];
|
|
505
496
|
/** The dispatching issue's title and body — the attribution source. */
|
|
506
497
|
issueText: string;
|
|
507
498
|
diff: PrDiff;
|
|
508
499
|
}
|
|
509
500
|
|
|
510
501
|
/**
|
|
511
|
-
* Every advisory finding about one settled pull request, in a stable order
|
|
512
|
-
*
|
|
502
|
+
* Every advisory finding about one settled pull request, in a stable order
|
|
503
|
+
* (test weakening in diff order).
|
|
504
|
+
*
|
|
505
|
+
* The file-list disclosure is not audited here at all: it is derived from
|
|
506
|
+
* `diff` itself ({@link deriveChangedLine}), so the `undisclosed-file` /
|
|
507
|
+
* `unmatched-claim` reconciliation flags are structurally gone. What remains
|
|
508
|
+
* is the diff-content check a human is worst placed to do by hand.
|
|
513
509
|
*
|
|
514
510
|
* An empty array is the expected result for an honest run, and is load-bearing:
|
|
515
511
|
* the settlement report says nothing at all when this returns nothing.
|
|
516
512
|
*/
|
|
517
513
|
export function analyseSettlement(audit: SettlementAudit): SettlementFlag[] {
|
|
518
514
|
const flags: SettlementFlag[] = [];
|
|
519
|
-
reconcileChanged(audit, flags);
|
|
520
515
|
detectWeakening(audit, flags);
|
|
521
516
|
return flags;
|
|
522
517
|
}
|
|
523
518
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
// every file is undisclosed, and saying so once beats saying it per file.
|
|
537
|
-
// Reporting it at all is what stops the check being defeated by writing
|
|
538
|
-
// `changed: see the PR` — or by dropping the line entirely.
|
|
539
|
-
if (touched.length > 0) {
|
|
540
|
-
flags.push({
|
|
541
|
-
kind: "changed-line-missing",
|
|
542
|
-
file: "(report)",
|
|
543
|
-
detail:
|
|
544
|
-
"the report's `changed:` line is missing or names no paths, so none of the " +
|
|
545
|
-
`${touched.length} file(s) the PR touched was disclosed`,
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
for (const file of touched) {
|
|
552
|
-
if (coverage.some((claim) => covers(claim, file.path))) continue;
|
|
553
|
-
if (file.previousPath !== undefined && coverage.some((claim) => covers(claim, file.previousPath ?? ""))) {
|
|
554
|
-
continue;
|
|
555
|
-
}
|
|
556
|
-
flags.push({
|
|
557
|
-
kind: "undisclosed-file",
|
|
558
|
-
file: file.path,
|
|
559
|
-
detail: `${file.status} by the PR but absent from the report's \`changed:\` line`,
|
|
560
|
-
});
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
// The weaker direction, and it stays weak on purpose: a worker often names a
|
|
564
|
-
// file it edited and then reverted, or one it renamed away from. Worth
|
|
565
|
-
// surfacing because a `changed:` line that describes a different PR is the
|
|
566
|
-
// signature of a report written from memory rather than from `git diff`.
|
|
567
|
-
//
|
|
568
|
-
// It iterates only the CURRENT report's claims (#199). A file claimed in
|
|
569
|
-
// attempt 1 and reverted by attempt 3 was true then and false now; pooling
|
|
570
|
-
// priors into this loop would turn every honest revert into a fresh finding.
|
|
571
|
-
for (const claim of claims) {
|
|
572
|
-
if (audit.diff.files.some((f) => covers(claim, f.path) || covers(claim, f.previousPath ?? ""))) {
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
if (extensionAlternatives.has(claim)) {
|
|
576
|
-
const stem = claim.slice(0, claim.lastIndexOf("."));
|
|
577
|
-
const matchedAlternative = claims.some(
|
|
578
|
-
(other) =>
|
|
579
|
-
other !== claim &&
|
|
580
|
-
extensionAlternatives.has(other) &&
|
|
581
|
-
other.slice(0, other.lastIndexOf(".")) === stem &&
|
|
582
|
-
audit.diff.files.some((f) => covers(other, f.path) || covers(other, f.previousPath ?? "")),
|
|
583
|
-
);
|
|
584
|
-
if (matchedAlternative) continue;
|
|
585
|
-
}
|
|
586
|
-
flags.push({
|
|
587
|
-
kind: "unmatched-claim",
|
|
588
|
-
file: claim,
|
|
589
|
-
detail: "named by the report's `changed:` line but not touched by the PR",
|
|
590
|
-
});
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// A parser failure can otherwise accuse the report in both directions for
|
|
594
|
-
// the same file. Collapse only that self-refuting pair; unrelated findings
|
|
595
|
-
// remain intact.
|
|
596
|
-
const unmatched = flags.filter((flag) => flag.kind === "unmatched-claim");
|
|
597
|
-
const removed = new Set<SettlementFlag>();
|
|
598
|
-
let exampleClaim = "";
|
|
599
|
-
let examplePath = "";
|
|
600
|
-
for (const undisclosed of flags.filter((flag) => flag.kind === "undisclosed-file")) {
|
|
601
|
-
const name = basename(undisclosed.file);
|
|
602
|
-
const extension = name.lastIndexOf(".");
|
|
603
|
-
const stem = extension > 0 ? name.slice(0, extension) : name;
|
|
604
|
-
if (stem.length < 3) continue;
|
|
605
|
-
for (const claim of unmatched) {
|
|
606
|
-
if (!claim.file.includes(stem)) continue;
|
|
607
|
-
removed.add(undisclosed);
|
|
608
|
-
removed.add(claim);
|
|
609
|
-
if (exampleClaim === "") {
|
|
610
|
-
exampleClaim = claim.file;
|
|
611
|
-
examplePath = undisclosed.file;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
if (removed.size === 0) return;
|
|
616
|
-
|
|
617
|
-
const kept = flags.filter((flag) => !removed.has(flag));
|
|
618
|
-
const removedClaims = unmatched.filter((flag) => removed.has(flag)).length;
|
|
619
|
-
flags.splice(
|
|
620
|
-
0,
|
|
621
|
-
flags.length,
|
|
622
|
-
...kept,
|
|
623
|
-
{
|
|
624
|
-
kind: "report-format-unparsed",
|
|
625
|
-
file: "(report)",
|
|
626
|
-
detail:
|
|
627
|
-
`${removedClaims} claim(s) on the \`changed:\` line could not be parsed as paths yet name the same ` +
|
|
628
|
-
`file(s) the PR touched (e.g. ${exampleClaim} vs ${examplePath}) — read the diff directly`,
|
|
629
|
-
},
|
|
630
|
-
);
|
|
631
|
-
}
|
|
519
|
+
/**
|
|
520
|
+
* The one finding a settlement can carry without a diff at all. `changed-line-missing`
|
|
521
|
+
* used to mean "the worker's report disclosed nothing"; with the list derived
|
|
522
|
+
* from the PR's own diff it can only mean the tree could not be read — a real
|
|
523
|
+
* fault worth surfacing, never an empty list that reads as a clean bill.
|
|
524
|
+
*/
|
|
525
|
+
export const UNREADABLE_TREE_FLAG: SettlementFlag = {
|
|
526
|
+
kind: "changed-line-missing",
|
|
527
|
+
file: "(report)",
|
|
528
|
+
detail:
|
|
529
|
+
"the settlement could not read the PR's diff, so no `changed:` file list could be derived",
|
|
530
|
+
};
|
|
632
531
|
|
|
633
532
|
function detectWeakening(audit: SettlementAudit, flags: SettlementFlag[]): void {
|
|
634
533
|
// A test file that left one path and arrived at another is a move, not a
|
|
@@ -788,30 +687,21 @@ const HEADING = "settlement audit";
|
|
|
788
687
|
* that could not be read in full — then the absence of flags is not evidence of
|
|
789
688
|
* anything, and saying so is the honest report.
|
|
790
689
|
*/
|
|
791
|
-
/** Appended to the heading when a multi-attempt audit pooled prior disclosures,
|
|
792
|
-
* so a reader is told the coverage is wider than this one report (#199). */
|
|
793
|
-
function pooledHeading(attempts: number | undefined): string {
|
|
794
|
-
return attempts !== undefined && attempts > 1
|
|
795
|
-
? ` — disclosures pooled across ${attempts} attempt(s)`
|
|
796
|
-
: "";
|
|
797
|
-
}
|
|
798
|
-
|
|
799
690
|
export function formatSettlementFlags(
|
|
800
691
|
flags: readonly SettlementFlag[],
|
|
801
|
-
diff: { truncated: boolean
|
|
692
|
+
diff: { truncated: boolean } = { truncated: false },
|
|
802
693
|
): string[] {
|
|
803
694
|
if (flags.length === 0) {
|
|
804
695
|
return diff.truncated
|
|
805
696
|
? [
|
|
806
697
|
`${HEADING}: no flags, but the PR diff was too large to read in full` +
|
|
807
|
-
|
|
698
|
+
" — this is not a clean bill",
|
|
808
699
|
]
|
|
809
700
|
: [];
|
|
810
701
|
}
|
|
811
702
|
const lines = [
|
|
812
703
|
`${HEADING}: ${flags.length} advisory flag(s) — the run's state is unchanged by them` +
|
|
813
|
-
(diff.truncated ? ", and the PR diff was too large to read in full" : "")
|
|
814
|
-
pooledHeading(diff.attempts),
|
|
704
|
+
(diff.truncated ? ", and the PR diff was too large to read in full" : ""),
|
|
815
705
|
];
|
|
816
706
|
for (const flag of flags.slice(0, RENDERED_FLAGS)) {
|
|
817
707
|
if (flag.kind === "pr-adopted") {
|