@wildorder/nightshift 0.8.0 → 0.9.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/as-built.d.ts +125 -0
- package/dist/as-built.d.ts.map +1 -0
- package/dist/as-built.js +322 -0
- package/dist/as-built.js.map +1 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +2 -2
- package/dist/run-program.d.ts +66 -2
- package/dist/run-program.d.ts.map +1 -1
- package/dist/run-program.js +322 -35
- package/dist/run-program.js.map +1 -1
- package/dist/whole-program-review.d.ts +165 -0
- package/dist/whole-program-review.d.ts.map +1 -0
- package/dist/whole-program-review.js +580 -0
- package/dist/whole-program-review.js.map +1 -0
- package/package.json +2 -2
package/dist/run-program.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { type AgentRunner, type VerifyRunner } from "./agent-runner.js";
|
|
2
2
|
import { type AuthorStageResult } from "./author.js";
|
|
3
|
-
import type { NightshiftConfig } from "./config.js";
|
|
3
|
+
import type { AgentConfig, NightshiftConfig } from "./config.js";
|
|
4
4
|
import { type DecisionLedgerView } from "./decision-ledger.js";
|
|
5
5
|
import { type RenderableRecord } from "./decision-view.js";
|
|
6
6
|
import { type ReviewPassOutcome } from "./review-pass.js";
|
|
7
7
|
import { type ProgramManifest, type Workstream } from "./manifest.js";
|
|
8
|
+
import { type AsBuiltFs, type WholeProgramReviewOutcome } from "./whole-program-review.js";
|
|
8
9
|
/**
|
|
9
10
|
* Git operations the runner needs, injectable so tests drive a real
|
|
10
11
|
* temporary repository or a fake. The runner owns commits — workstream
|
|
@@ -32,6 +33,8 @@ export interface GitOps {
|
|
|
32
33
|
dirtyPaths(cwd: string): Promise<string[]>;
|
|
33
34
|
/** True when `ancestor` is an ancestor of (or equal to) `descendant`. */
|
|
34
35
|
isAncestor(cwd: string, ancestor: string, descendant: string): Promise<boolean>;
|
|
36
|
+
/** `git merge-base <a> <b>`; undefined when they share no ancestor or either is unknown. */
|
|
37
|
+
mergeBase(cwd: string, a: string, b: string): Promise<string | undefined>;
|
|
35
38
|
/** Moves the current branch to `commit`, discarding the working tree. */
|
|
36
39
|
resetHard(cwd: string, commit: string): Promise<void>;
|
|
37
40
|
/** Points `ref` at `commit`, creating or moving it. */
|
|
@@ -95,6 +98,8 @@ export interface RunProgramResult {
|
|
|
95
98
|
* same set the run report's "Needs your attention" section renders. */
|
|
96
99
|
escalations: RenderableRecord[];
|
|
97
100
|
reportPath: string;
|
|
101
|
+
/** The end-of-run whole-program review stage's result, for a caller that wants the snapshot commit. */
|
|
102
|
+
wholeProgramReview?: WholeProgramStageResult;
|
|
98
103
|
}
|
|
99
104
|
/** Every workstream in the transitive downstream cone of the given ids. */
|
|
100
105
|
export declare function downstreamCone(workstreams: readonly Workstream[], rootIds: readonly string[]): Set<string>;
|
|
@@ -109,6 +114,65 @@ export declare function downstreamCone(workstreams: readonly Workstream[], rootI
|
|
|
109
114
|
* verify commands itself, and writes the run report last, whatever happened.
|
|
110
115
|
*/
|
|
111
116
|
export declare function runProgram(options: RunProgramOptions): Promise<RunProgramResult>;
|
|
117
|
+
/**
|
|
118
|
+
* Resolves the program's diff base for the whole-program review: the
|
|
119
|
+
* merge-base with the default branch, when it exists and differs from HEAD
|
|
120
|
+
* (the program's real branch point — the same range a pull request shows a
|
|
121
|
+
* human); otherwise `runStartCommit`, when it differs from HEAD (a
|
|
122
|
+
* `--force` run made directly on the default branch, where the merge-base is
|
|
123
|
+
* HEAD itself); otherwise `undefined` — an ordinary outcome, not a failure.
|
|
124
|
+
*/
|
|
125
|
+
export declare function resolveProgramDiffBase(args: {
|
|
126
|
+
root: string;
|
|
127
|
+
git: GitOps;
|
|
128
|
+
isRepository: boolean;
|
|
129
|
+
/** HEAD as it stood before the run's first agent ran. */
|
|
130
|
+
runStartCommit: string | undefined;
|
|
131
|
+
}): Promise<string | undefined>;
|
|
132
|
+
/** The synthetic workstream id whole-program findings are journaled under (SC-07). */
|
|
133
|
+
export declare const WHOLE_PROGRAM_SUBJECT = "whole-program";
|
|
134
|
+
export interface WholeProgramStageResult {
|
|
135
|
+
outcome: WholeProgramReviewOutcome;
|
|
136
|
+
/** The snapshot commit, when one landed. */
|
|
137
|
+
commit?: string;
|
|
138
|
+
/** True when a commit was skipped because the tree is not a git repository. */
|
|
139
|
+
commitSkipped: boolean;
|
|
140
|
+
/**
|
|
141
|
+
* Ledger ids this stage journaled, for the fix-now cross-reference. Empty
|
|
142
|
+
* when the append failed — these are ids that are *in* the ledger.
|
|
143
|
+
*/
|
|
144
|
+
findingIds: string[];
|
|
145
|
+
/** Post-pass failures — commit, ledger append, triage. Reported, never fatal. */
|
|
146
|
+
errors: string[];
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The end-of-run whole-program review stage: establishes the diff base,
|
|
150
|
+
* calls WS-01's never-throwing pass exactly once, commits exactly what it
|
|
151
|
+
* wrote, and routes its findings through the existing findings path. Every
|
|
152
|
+
* risky step around the pass gets its own catch so a stage failure is a
|
|
153
|
+
* sentence in the report, never a change to the run's outcome (SC-05,
|
|
154
|
+
* SC-12) — see the outer catch below for the belt-and-braces case where a
|
|
155
|
+
* bug in this function's own body throws before the pass has even run.
|
|
156
|
+
*/
|
|
157
|
+
export declare function runWholeProgramStage(args: {
|
|
158
|
+
root: string;
|
|
159
|
+
programId: string;
|
|
160
|
+
manifest: ProgramManifest;
|
|
161
|
+
config: NightshiftConfig;
|
|
162
|
+
agentRunner: AgentRunner;
|
|
163
|
+
git: GitOps;
|
|
164
|
+
isRepository: boolean;
|
|
165
|
+
results: WorkstreamResult[];
|
|
166
|
+
authorResult: AuthorStageResult;
|
|
167
|
+
runStartCommit: string | undefined;
|
|
168
|
+
decider: AgentConfig | undefined;
|
|
169
|
+
reviewed: Set<string>;
|
|
170
|
+
triaged: Set<string>;
|
|
171
|
+
now: () => Date;
|
|
172
|
+
log: (line: string) => void;
|
|
173
|
+
/** Test seam only; production never sets it. Passed straight to the pass. */
|
|
174
|
+
fs?: AsBuiltFs;
|
|
175
|
+
}): Promise<WholeProgramStageResult>;
|
|
112
176
|
export interface FixAttempt {
|
|
113
177
|
/** "kept" = the attempt verified clean under the runner's configured
|
|
114
178
|
* commands; "failed" = it did not. Mirrors the critique loop's existing
|
|
@@ -126,5 +190,5 @@ export interface FixAttempt {
|
|
|
126
190
|
/** The verification diagnosis, present only when `outcome === "failed"`. */
|
|
127
191
|
failure?: string;
|
|
128
192
|
}
|
|
129
|
-
export declare function renderRunReport(manifest: ProgramManifest, results: WorkstreamResult[], ledger: DecisionLedgerView, triagedThisRun: ReadonlySet<string>, authorResult: AuthorStageResult, at: Date): string;
|
|
193
|
+
export declare function renderRunReport(manifest: ProgramManifest, results: WorkstreamResult[], ledger: DecisionLedgerView, triagedThisRun: ReadonlySet<string>, authorResult: AuthorStageResult, at: Date, wholeProgram?: WholeProgramStageResult): string;
|
|
130
194
|
//# sourceMappingURL=run-program.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-program.d.ts","sourceRoot":"","sources":["../src/run-program.ts"],"names":[],"mappings":"AAIA,OAAO,EAUL,KAAK,WAAW,EAChB,KAAK,YAAY,EAClB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAiB,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACpE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"run-program.d.ts","sourceRoot":"","sources":["../src/run-program.ts"],"names":[],"mappings":"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"}
|
package/dist/run-program.js
CHANGED
|
@@ -17,6 +17,7 @@ import { detectDefaultBranch, programBranchName } from "./program-branch.js";
|
|
|
17
17
|
import { restoreProgramsDir, snapshotProgramsDir } from "./programs-dir.js";
|
|
18
18
|
import { CouldNotStartError } from "./exit-codes.js";
|
|
19
19
|
import { runReportPath } from "./report-path.js";
|
|
20
|
+
import { AS_BUILT_PATH, runWholeProgramReview, renderWholeProgramReview, } from "./whole-program-review.js";
|
|
20
21
|
const execFileAsync = promisify(execFile);
|
|
21
22
|
/** Matches every wording git uses to report an empty commit attempt. */
|
|
22
23
|
const NOTHING_TO_COMMIT = /nothing to commit|nothing added to commit|no changes added to commit/u;
|
|
@@ -111,6 +112,15 @@ export const defaultGitOps = {
|
|
|
111
112
|
return false;
|
|
112
113
|
}
|
|
113
114
|
},
|
|
115
|
+
async mergeBase(cwd, a, b) {
|
|
116
|
+
try {
|
|
117
|
+
const { stdout } = await execFileAsync("git", ["merge-base", a, b], { cwd });
|
|
118
|
+
return stdout.trim();
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
114
124
|
async resetHard(cwd, commit) {
|
|
115
125
|
await execFileAsync("git", ["reset", "--hard", commit], { cwd });
|
|
116
126
|
},
|
|
@@ -299,8 +309,8 @@ export async function runProgram(options) {
|
|
|
299
309
|
? `decider: ${describeAgent(decider)}`
|
|
300
310
|
: "decider: none configured — implementer defaults will stand unratified");
|
|
301
311
|
log(reviewer
|
|
302
|
-
? `test critique
|
|
303
|
-
: "
|
|
312
|
+
? `reviewer (test critique, whole-program review): ${describeAgent(reviewer)}`
|
|
313
|
+
: "reviewer: none configured — test critique and the whole-program review are disabled");
|
|
304
314
|
const cycles = findCycles(manifest.workstreams);
|
|
305
315
|
if (cycles.length > 0) {
|
|
306
316
|
// A cyclic graph cannot be ordered; this is a planning defect, not a
|
|
@@ -356,6 +366,11 @@ export async function runProgram(options) {
|
|
|
356
366
|
}
|
|
357
367
|
}
|
|
358
368
|
}
|
|
369
|
+
// HEAD as it stood before the run's first agent ran — the whole-program
|
|
370
|
+
// review stage's fallback diff base (see resolveProgramDiffBase) when a
|
|
371
|
+
// resumed run's merge-base with the default branch is unavailable or
|
|
372
|
+
// equal to HEAD.
|
|
373
|
+
const runStartCommit = isRepository ? await git.currentCommit(root) : undefined;
|
|
359
374
|
// Subject ids already sent to the decider this run — shared across the
|
|
360
375
|
// authoring and build stages (a run is one process) and across the
|
|
361
376
|
// decision and finding paths, so "once per subject per run" (SC-10) is
|
|
@@ -430,23 +445,51 @@ export async function runProgram(options) {
|
|
|
430
445
|
}
|
|
431
446
|
}
|
|
432
447
|
}
|
|
433
|
-
const ledger = await readDecisionLedger(root, options.programId);
|
|
434
|
-
const escalations = escalatedRecords(ledger);
|
|
435
448
|
const complete = results.every((result) => result.outcome.status === "complete" ||
|
|
436
449
|
result.outcome.status === "skipped");
|
|
437
450
|
manifest.program.status = complete ? "complete" : "partial";
|
|
438
451
|
await saveManifest(root, options.programId, manifest);
|
|
452
|
+
const wholeProgram = await runWholeProgramStage({
|
|
453
|
+
root,
|
|
454
|
+
programId: options.programId,
|
|
455
|
+
manifest,
|
|
456
|
+
config,
|
|
457
|
+
agentRunner,
|
|
458
|
+
git,
|
|
459
|
+
isRepository,
|
|
460
|
+
results,
|
|
461
|
+
authorResult,
|
|
462
|
+
runStartCommit,
|
|
463
|
+
decider,
|
|
464
|
+
reviewed,
|
|
465
|
+
triaged,
|
|
466
|
+
now,
|
|
467
|
+
log,
|
|
468
|
+
});
|
|
469
|
+
const ledger = await readDecisionLedger(root, options.programId);
|
|
470
|
+
const escalations = escalatedRecords(ledger);
|
|
439
471
|
const reportPath = runReportPath(root, options.programId);
|
|
440
|
-
|
|
441
|
-
//
|
|
442
|
-
//
|
|
443
|
-
//
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
472
|
+
// Every workstream verdict, `complete`, and the exit-code mapping are
|
|
473
|
+
// already settled above; writing and committing the report is bookkeeping
|
|
474
|
+
// that must not be able to reject `runProgram` after the fact (SC-05,
|
|
475
|
+
// SC-12) — a full disk or an EISDIR here is a logged line, not a thrown
|
|
476
|
+
// run.
|
|
477
|
+
try {
|
|
478
|
+
await writeFile(reportPath,
|
|
479
|
+
// `triaged` is the run-local set of subject ids the decider actually
|
|
480
|
+
// ruled on this run (built up across the authoring, build, and
|
|
481
|
+
// whole-program stages, see its declaration above) — exactly the
|
|
482
|
+
// `triagedThisRun` basis the "this run" triage ratio needs, since the
|
|
483
|
+
// projected ledger carries no run identifier of its own. It excludes
|
|
484
|
+
// ids that were merely sent but whose invocation failed or returned no
|
|
485
|
+
// valid verdict.
|
|
486
|
+
renderRunReport(manifest, results, ledger, triaged, authorResult, now(), wholeProgram), "utf8");
|
|
487
|
+
if (isRepository) {
|
|
488
|
+
await git.commitPaths(root, `nightshift(${options.programId}): run report and decision ledger`, ["docs/programs"]);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch (error) {
|
|
492
|
+
log(`run report: could not write or commit ${reportPath}: ${error.message}`);
|
|
450
493
|
}
|
|
451
494
|
log(`run report: ${reportPath}`);
|
|
452
495
|
return {
|
|
@@ -455,6 +498,7 @@ export async function runProgram(options) {
|
|
|
455
498
|
workstreams: results,
|
|
456
499
|
escalations,
|
|
457
500
|
reportPath,
|
|
501
|
+
wholeProgramReview: wholeProgram,
|
|
458
502
|
};
|
|
459
503
|
async function runWorkstream(workstream) {
|
|
460
504
|
const base = {
|
|
@@ -762,6 +806,192 @@ export async function runProgram(options) {
|
|
|
762
806
|
return kept ? fix.greenCommit : greenCommit;
|
|
763
807
|
}
|
|
764
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* Resolves the program's diff base for the whole-program review: the
|
|
811
|
+
* merge-base with the default branch, when it exists and differs from HEAD
|
|
812
|
+
* (the program's real branch point — the same range a pull request shows a
|
|
813
|
+
* human); otherwise `runStartCommit`, when it differs from HEAD (a
|
|
814
|
+
* `--force` run made directly on the default branch, where the merge-base is
|
|
815
|
+
* HEAD itself); otherwise `undefined` — an ordinary outcome, not a failure.
|
|
816
|
+
*/
|
|
817
|
+
export async function resolveProgramDiffBase(args) {
|
|
818
|
+
const { root, git, isRepository, runStartCommit } = args;
|
|
819
|
+
if (!isRepository)
|
|
820
|
+
return undefined;
|
|
821
|
+
const head = await git.currentCommit(root);
|
|
822
|
+
if (head === undefined)
|
|
823
|
+
return undefined;
|
|
824
|
+
const defaultBranch = await detectDefaultBranch(root);
|
|
825
|
+
const base = await git.mergeBase(root, defaultBranch, head);
|
|
826
|
+
if (base !== undefined && base !== head)
|
|
827
|
+
return base;
|
|
828
|
+
if (runStartCommit !== undefined && runStartCommit !== head)
|
|
829
|
+
return runStartCommit;
|
|
830
|
+
return undefined;
|
|
831
|
+
}
|
|
832
|
+
/** The synthetic workstream id whole-program findings are journaled under (SC-07). */
|
|
833
|
+
export const WHOLE_PROGRAM_SUBJECT = "whole-program";
|
|
834
|
+
function emptyWholeProgramSeverityCounts() {
|
|
835
|
+
return { blocker: 0, major: 0, minor: 0, advisory: 0 };
|
|
836
|
+
}
|
|
837
|
+
/** The placeholder outcome for the (practically unreachable) case where the stage's own body throws before the pass ever runs. */
|
|
838
|
+
function stageFailureOutcome(reason) {
|
|
839
|
+
return {
|
|
840
|
+
ran: false,
|
|
841
|
+
status: "reviewer-error",
|
|
842
|
+
refreshed: false,
|
|
843
|
+
writtenPaths: [],
|
|
844
|
+
findings: [],
|
|
845
|
+
errors: [],
|
|
846
|
+
severityCounts: emptyWholeProgramSeverityCounts(),
|
|
847
|
+
lengthOverrun: false,
|
|
848
|
+
missingLimitations: [],
|
|
849
|
+
inputClipped: false,
|
|
850
|
+
reason,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
/** Failed and parked workstreams only (SC-08) — a `skipped` result means already complete, i.e. built. */
|
|
854
|
+
function notBuiltRoster(results, authorResult) {
|
|
855
|
+
const authorById = new Map(authorResult.results.map((entry) => [entry.id, entry]));
|
|
856
|
+
const notBuilt = [];
|
|
857
|
+
for (const result of results) {
|
|
858
|
+
const outcome = result.outcome;
|
|
859
|
+
if (outcome.status !== "failed" && outcome.status !== "parked")
|
|
860
|
+
continue;
|
|
861
|
+
const authorOutcome = authorById.get(result.id)?.outcome;
|
|
862
|
+
const reason = authorOutcome !== undefined &&
|
|
863
|
+
(authorOutcome.status === "failed" || authorOutcome.status === "parked")
|
|
864
|
+
? `${outcome.reason} (spec authoring: ${authorOutcome.reason})`
|
|
865
|
+
: outcome.reason;
|
|
866
|
+
notBuilt.push({
|
|
867
|
+
id: result.id,
|
|
868
|
+
name: result.name,
|
|
869
|
+
status: outcome.status,
|
|
870
|
+
reason,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
873
|
+
return notBuilt;
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* The end-of-run whole-program review stage: establishes the diff base,
|
|
877
|
+
* calls WS-01's never-throwing pass exactly once, commits exactly what it
|
|
878
|
+
* wrote, and routes its findings through the existing findings path. Every
|
|
879
|
+
* risky step around the pass gets its own catch so a stage failure is a
|
|
880
|
+
* sentence in the report, never a change to the run's outcome (SC-05,
|
|
881
|
+
* SC-12) — see the outer catch below for the belt-and-braces case where a
|
|
882
|
+
* bug in this function's own body throws before the pass has even run.
|
|
883
|
+
*/
|
|
884
|
+
export async function runWholeProgramStage(args) {
|
|
885
|
+
const { root, programId, manifest, config, agentRunner, git, isRepository, results, authorResult, runStartCommit, decider, reviewed, triaged, now, log, fs, } = args;
|
|
886
|
+
const errors = [];
|
|
887
|
+
let commit;
|
|
888
|
+
let commitSkipped = false;
|
|
889
|
+
let findingIds = [];
|
|
890
|
+
let outcome = stageFailureOutcome("the whole-program review stage did not complete");
|
|
891
|
+
// The caller's log callback is itself a risky step — it is not this
|
|
892
|
+
// stage's own code and can throw (a full log file, a broken transport).
|
|
893
|
+
// Both of the stage's own lines go through this wrapper rather than `log`
|
|
894
|
+
// directly, so a throwing logger costs one sentence in `errors` and never
|
|
895
|
+
// aborts the commit or the findings routing that follow it.
|
|
896
|
+
const safeLog = (line) => {
|
|
897
|
+
try {
|
|
898
|
+
log(line);
|
|
899
|
+
}
|
|
900
|
+
catch (error) {
|
|
901
|
+
errors.push(`the whole-program review logger failed: ${error.message}`);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
try {
|
|
905
|
+
safeLog("whole-program review: starting");
|
|
906
|
+
const base = await resolveProgramDiffBase({ root, git, isRepository, runStartCommit });
|
|
907
|
+
const diff = base === undefined ? "" : await git.diffSince(root, base);
|
|
908
|
+
const notBuilt = notBuiltRoster(results, authorResult);
|
|
909
|
+
const reviewCommit = isRepository ? await git.currentCommit(root) : undefined;
|
|
910
|
+
outcome = await runWholeProgramReview({
|
|
911
|
+
root,
|
|
912
|
+
programId,
|
|
913
|
+
manifest,
|
|
914
|
+
config,
|
|
915
|
+
agentRunner,
|
|
916
|
+
diff,
|
|
917
|
+
...(base === undefined ? {} : { baseCommit: base }),
|
|
918
|
+
...(notBuilt.length === 0 ? {} : { notBuilt }),
|
|
919
|
+
...(fs === undefined ? {} : { fs }),
|
|
920
|
+
log,
|
|
921
|
+
});
|
|
922
|
+
safeLog(outcome.status === "refreshed"
|
|
923
|
+
? `whole-program review: refreshed ${AS_BUILT_PATH}` +
|
|
924
|
+
(outcome.archivedTo ? ` (archived to ${outcome.archivedTo})` : "")
|
|
925
|
+
: `whole-program review: ${outcome.status}` +
|
|
926
|
+
(outcome.reason ? ` — ${outcome.reason}` : ""));
|
|
927
|
+
if (outcome.writtenPaths.length > 0) {
|
|
928
|
+
if (isRepository) {
|
|
929
|
+
try {
|
|
930
|
+
commit = await git.commitPaths(root, `nightshift(${programId}): as-built snapshot`, outcome.writtenPaths);
|
|
931
|
+
}
|
|
932
|
+
catch (error) {
|
|
933
|
+
errors.push(`could not commit the as-built snapshot: ${error.message}`);
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
commitSkipped = true;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
const events = findingsToLedgerEvents({
|
|
941
|
+
workstreamId: WHOLE_PROGRAM_SUBJECT,
|
|
942
|
+
findings: outcome.findings.filter(hasRoutableEvidence),
|
|
943
|
+
...(reviewCommit === undefined ? {} : { baseCommit: reviewCommit }),
|
|
944
|
+
now,
|
|
945
|
+
});
|
|
946
|
+
let journaled = false;
|
|
947
|
+
if (events.length === 0) {
|
|
948
|
+
journaled = true;
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
try {
|
|
952
|
+
await appendLedgerEvents(root, programId, events);
|
|
953
|
+
journaled = true;
|
|
954
|
+
findingIds = events.map((event) => event.id);
|
|
955
|
+
}
|
|
956
|
+
catch (error) {
|
|
957
|
+
errors.push(`could not journal the whole-program findings: ${error.message}`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (journaled && events.length > 0) {
|
|
961
|
+
try {
|
|
962
|
+
await triageFindings({
|
|
963
|
+
root,
|
|
964
|
+
programId,
|
|
965
|
+
manifest,
|
|
966
|
+
workstreamId: WHOLE_PROGRAM_SUBJECT,
|
|
967
|
+
findings: events,
|
|
968
|
+
baseCommit: reviewCommit,
|
|
969
|
+
decider,
|
|
970
|
+
agentRunner,
|
|
971
|
+
git,
|
|
972
|
+
isRepository,
|
|
973
|
+
reviewed,
|
|
974
|
+
triaged,
|
|
975
|
+
now,
|
|
976
|
+
log,
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
catch (error) {
|
|
980
|
+
errors.push(`the decider failed to triage the whole-program findings: ${error.message}`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
catch (error) {
|
|
985
|
+
errors.push(`the whole-program review stage failed unexpectedly: ${error.message}`);
|
|
986
|
+
}
|
|
987
|
+
return {
|
|
988
|
+
outcome,
|
|
989
|
+
...(commit === undefined ? {} : { commit }),
|
|
990
|
+
commitSkipped,
|
|
991
|
+
findingIds,
|
|
992
|
+
errors,
|
|
993
|
+
};
|
|
994
|
+
}
|
|
765
995
|
/** Undefined means the attempt verified clean; otherwise the diagnosis. */
|
|
766
996
|
async function verifyAttempt(config, verifyRunner, root, agentExitCode) {
|
|
767
997
|
if (agentExitCode !== 0) {
|
|
@@ -1173,36 +1403,36 @@ function renderRawFinding(finding, workstreamLabel, sinceFixed) {
|
|
|
1173
1403
|
return `- **${finding.subject}** (${finding.severity}, ${workstreamLabel}) — ${finding.message}${suffix}`;
|
|
1174
1404
|
}
|
|
1175
1405
|
/**
|
|
1176
|
-
*
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1179
|
-
* WS-02's own dedup ever let that happen) is not double-listed.
|
|
1406
|
+
* Minor/advisory findings from one list, deduped by subject/message/location.
|
|
1407
|
+
* Shared by `collectPassLimitations` (a pass's `open` and `resolved` findings)
|
|
1408
|
+
* and the whole-program review's flat `Finding[]`.
|
|
1180
1409
|
*/
|
|
1181
|
-
function
|
|
1182
|
-
if (!outcome)
|
|
1183
|
-
return [];
|
|
1410
|
+
function collectLimitationLines(findings, label, sinceFixed = false) {
|
|
1184
1411
|
const seen = new Set();
|
|
1185
1412
|
const lines = [];
|
|
1186
|
-
for (const finding of
|
|
1187
|
-
if (finding.severity !== "minor" && finding.severity !== "advisory")
|
|
1188
|
-
continue;
|
|
1189
|
-
const key = limitationKey(finding);
|
|
1190
|
-
if (seen.has(key))
|
|
1191
|
-
continue;
|
|
1192
|
-
seen.add(key);
|
|
1193
|
-
lines.push(renderRawFinding(finding, workstreamLabel, false));
|
|
1194
|
-
}
|
|
1195
|
-
for (const finding of outcome.resolved) {
|
|
1413
|
+
for (const finding of findings) {
|
|
1196
1414
|
if (finding.severity !== "minor" && finding.severity !== "advisory")
|
|
1197
1415
|
continue;
|
|
1198
1416
|
const key = limitationKey(finding);
|
|
1199
1417
|
if (seen.has(key))
|
|
1200
1418
|
continue;
|
|
1201
1419
|
seen.add(key);
|
|
1202
|
-
lines.push(renderRawFinding(finding,
|
|
1420
|
+
lines.push(renderRawFinding(finding, label, sinceFixed));
|
|
1203
1421
|
}
|
|
1204
1422
|
return lines;
|
|
1205
1423
|
}
|
|
1424
|
+
/**
|
|
1425
|
+
* A pass's minor/advisory findings, `open` and `resolved` alike (SC-07's
|
|
1426
|
+
* "every ... raised" — a finding the writer fixed was still raised).
|
|
1427
|
+
*/
|
|
1428
|
+
function collectPassLimitations(outcome, workstreamLabel) {
|
|
1429
|
+
if (!outcome)
|
|
1430
|
+
return [];
|
|
1431
|
+
return [
|
|
1432
|
+
...collectLimitationLines(outcome.open, workstreamLabel),
|
|
1433
|
+
...collectLimitationLines(outcome.resolved, workstreamLabel, true),
|
|
1434
|
+
];
|
|
1435
|
+
}
|
|
1206
1436
|
/**
|
|
1207
1437
|
* One aggregated section (SC-07): every minor/advisory finding raised
|
|
1208
1438
|
* anywhere in the run, every finding the decider accepted, any finding
|
|
@@ -1210,11 +1440,14 @@ function collectPassLimitations(outcome, workstreamLabel) {
|
|
|
1210
1440
|
* every fix-now attempt, and the run-local triage ratio. Fail-open
|
|
1211
1441
|
* throughout — an empty run renders a plain statement, never nothing.
|
|
1212
1442
|
*/
|
|
1213
|
-
function renderKnownLimitations(results, authorResult, ledger, escalated, triagedThisRun, programId) {
|
|
1443
|
+
function renderKnownLimitations(results, authorResult, ledger, escalated, triagedThisRun, programId, wholeProgram) {
|
|
1214
1444
|
const lines = ["## Known limitations", ""];
|
|
1215
1445
|
const passLimitations = [
|
|
1216
1446
|
...results.flatMap((result) => collectPassLimitations(result.testCritique, `${result.id} test critique`)),
|
|
1217
1447
|
...authorResult.results.flatMap((entry) => collectPassLimitations(entry.specCritique, `${entry.id} spec critique`)),
|
|
1448
|
+
...(wholeProgram
|
|
1449
|
+
? collectLimitationLines(wholeProgram.outcome.findings, "whole-program review")
|
|
1450
|
+
: []),
|
|
1218
1451
|
];
|
|
1219
1452
|
const acceptedFindings = ledger.findings.filter((record) => record.status === "accepted");
|
|
1220
1453
|
const openFindings = ledger.findings.filter((record) => record.status === "open");
|
|
@@ -1259,6 +1492,10 @@ function renderKnownLimitations(results, authorResult, ledger, escalated, triage
|
|
|
1259
1492
|
: "";
|
|
1260
1493
|
lines.push(`- **${record.subject}** — fixed in the run${commitPart}: ${record.fixAttempt.note}`, "");
|
|
1261
1494
|
}
|
|
1495
|
+
else if (record.workstream === WHOLE_PROGRAM_SUBJECT) {
|
|
1496
|
+
lines.push(`- **${record.subject}** — from the whole-program review, triaged ` +
|
|
1497
|
+
"`fix-now`: carried to the next run; nothing was fixed in this one.", "");
|
|
1498
|
+
}
|
|
1262
1499
|
else {
|
|
1263
1500
|
lines.push(`- **${record.subject}** — fix-now triaged; outcome pending.`, "");
|
|
1264
1501
|
}
|
|
@@ -1270,7 +1507,54 @@ function renderKnownLimitations(results, authorResult, ledger, escalated, triage
|
|
|
1270
1507
|
`${escalatedThisRunCount} ${escalatedThisRunCount === 1 ? "was" : "were"} escalated.`, "");
|
|
1271
1508
|
return lines;
|
|
1272
1509
|
}
|
|
1273
|
-
|
|
1510
|
+
/** The commit fact WS-01's outcome cannot know — sha and paths, or why there is none. */
|
|
1511
|
+
function commitFactLines(wholeProgram) {
|
|
1512
|
+
const { commit, commitSkipped, outcome } = wholeProgram;
|
|
1513
|
+
if (commit !== undefined) {
|
|
1514
|
+
return [`Committed as \`${commit}\`: ${outcome.writtenPaths.join(", ")}.`];
|
|
1515
|
+
}
|
|
1516
|
+
if (outcome.writtenPaths.length === 0) {
|
|
1517
|
+
return ["No commit — nothing was written."];
|
|
1518
|
+
}
|
|
1519
|
+
if (commitSkipped) {
|
|
1520
|
+
return [
|
|
1521
|
+
"Written to the working tree but not committed: this is not a git repository.",
|
|
1522
|
+
];
|
|
1523
|
+
}
|
|
1524
|
+
return ["The snapshot was written, but committing it failed — see the stage error below."];
|
|
1525
|
+
}
|
|
1526
|
+
/**
|
|
1527
|
+
* A whole-program finding this stage journaled that the decider triaged
|
|
1528
|
+
* `fix-now` this run. Keys on `findingIds` (this run's own journaling), not
|
|
1529
|
+
* on `record.workstream` — that broader statement is §3.7's, in
|
|
1530
|
+
* `renderKnownLimitations`, and applies across runs.
|
|
1531
|
+
*/
|
|
1532
|
+
function carriedFixNowLines(wholeProgram, ledger) {
|
|
1533
|
+
if (wholeProgram.findingIds.length === 0)
|
|
1534
|
+
return [];
|
|
1535
|
+
const ids = new Set(wholeProgram.findingIds);
|
|
1536
|
+
const carried = ledger.findings.filter((record) => ids.has(record.id) && record.status === "fix-now");
|
|
1537
|
+
if (carried.length === 0)
|
|
1538
|
+
return [];
|
|
1539
|
+
return carried.map((record) => `Triaged \`fix-now\`: **${record.subject}** — carried to the next run; nothing was fixed in this one.`);
|
|
1540
|
+
}
|
|
1541
|
+
/**
|
|
1542
|
+
* The `## Whole-program review` section: delegates its body to WS-01's own
|
|
1543
|
+
* renderer verbatim, and adds only what that module cannot know — the commit
|
|
1544
|
+
* fact, this run's carried fix-now findings, and this stage's own errors.
|
|
1545
|
+
*/
|
|
1546
|
+
function renderWholeProgramSection(wholeProgram, ledger) {
|
|
1547
|
+
const lines = ["## Whole-program review", ""];
|
|
1548
|
+
lines.push(...renderWholeProgramReview(wholeProgram.outcome));
|
|
1549
|
+
lines.push(...commitFactLines(wholeProgram));
|
|
1550
|
+
lines.push(...carriedFixNowLines(wholeProgram, ledger));
|
|
1551
|
+
if (wholeProgram.errors.length > 0) {
|
|
1552
|
+
lines.push(...wholeProgram.errors.map((error) => `Stage error: ${error}`));
|
|
1553
|
+
}
|
|
1554
|
+
lines.push("");
|
|
1555
|
+
return lines;
|
|
1556
|
+
}
|
|
1557
|
+
export function renderRunReport(manifest, results, ledger, triagedThisRun, authorResult, at, wholeProgram) {
|
|
1274
1558
|
const programId = manifest.program.id;
|
|
1275
1559
|
const built = results.filter((result) => result.outcome.status === "complete" ||
|
|
1276
1560
|
result.outcome.status === "skipped").length;
|
|
@@ -1332,6 +1616,9 @@ export function renderRunReport(manifest, results, ledger, triagedThisRun, autho
|
|
|
1332
1616
|
}
|
|
1333
1617
|
}
|
|
1334
1618
|
lines.push("");
|
|
1619
|
+
if (wholeProgram !== undefined) {
|
|
1620
|
+
lines.push(...renderWholeProgramSection(wholeProgram, ledger));
|
|
1621
|
+
}
|
|
1335
1622
|
const settledDecisions = ledger.decisions.filter((record) => record.status !== "escalated");
|
|
1336
1623
|
lines.push("## Decisions made along the way", "", "Every judgment call an agent surfaced, with its review status.", "Anything here can be revisited: roll back to the anchor commit and", "re-run, or just say which option you want changed.", "");
|
|
1337
1624
|
if (settledDecisions.length === 0) {
|
|
@@ -1342,7 +1629,7 @@ export function renderRunReport(manifest, results, ledger, triagedThisRun, autho
|
|
|
1342
1629
|
lines.push(...renderRecord(record, { density: "compact", programId }), "");
|
|
1343
1630
|
}
|
|
1344
1631
|
}
|
|
1345
|
-
lines.push(...renderKnownLimitations(results, authorResult, ledger, escalated, triagedThisRun, programId));
|
|
1632
|
+
lines.push(...renderKnownLimitations(results, authorResult, ledger, escalated, triagedThisRun, programId, wholeProgram));
|
|
1346
1633
|
const decisionErrors = results.flatMap((result) => result.decisionErrors);
|
|
1347
1634
|
if (decisionErrors.length > 0) {
|
|
1348
1635
|
lines.push("## Decision blocks the runner could not read", "", ...decisionErrors.map((error) => `- ${error}`), "");
|