forgedock 1.30.0 → 1.32.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/bin/engine/outcome-codes.mjs +3 -2
- package/bin/engine/runlog.mjs +82 -12
- package/bin/engine/telemetry-seam.mjs +11 -4
- package/bin/engine/telemetry-seam.test.mjs +18 -0
- package/bin/engine-cli.mjs +409 -38
- package/bin/engine.mjs +47 -37
- package/bin/forgedock.mjs +35 -3
- package/bin/gc.mjs +50 -17
- package/bin/hooks/reread-nudge.mjs +65 -0
- package/bin/labels.json +5 -0
- package/bin/lean/apishape.mjs +170 -0
- package/bin/lean/apishape.test.mjs +187 -0
- package/bin/lean/batch.mjs +32 -0
- package/bin/lean/batch.test.mjs +338 -2
- package/bin/lean/budget.mjs +89 -0
- package/bin/lean/budget.test.mjs +106 -0
- package/bin/lean/checks.mjs +32 -9
- package/bin/lean/checks.test.mjs +78 -1
- package/bin/lean/conformance.e2e.test.mjs +27 -5
- package/bin/lean/conformance.mjs +150 -15
- package/bin/lean/conformance.test.mjs +175 -3
- package/bin/lean/context.mjs +128 -44
- package/bin/lean/context.test.mjs +157 -5
- package/bin/lean/decompose.mjs +90 -0
- package/bin/lean/decompose.test.mjs +105 -0
- package/bin/lean/depcache.mjs +11 -7
- package/bin/lean/depcache.test.mjs +81 -0
- package/bin/lean/e2e.mjs +102 -0
- package/bin/lean/e2e.test.mjs +86 -0
- package/bin/lean/effects.mjs +48 -15
- package/bin/lean/explore.mjs +47 -0
- package/bin/lean/explore.test.mjs +61 -0
- package/bin/lean/facts.mjs +84 -0
- package/bin/lean/facts.test.mjs +66 -0
- package/bin/lean/fixround.mjs +90 -0
- package/bin/lean/fixround.test.mjs +117 -0
- package/bin/lean/heavy.mjs +35 -2
- package/bin/lean/heavy.test.mjs +32 -2
- package/bin/lean/lean.mjs +712 -134
- package/bin/lean/lean.test.mjs +1626 -37
- package/bin/lean/ledger.mjs +5 -3
- package/bin/lean/ledger.test.mjs +18 -0
- package/bin/lean/light.mjs +135 -0
- package/bin/lean/light.test.mjs +173 -0
- package/bin/lean/lightlane.mjs +219 -0
- package/bin/lean/lightlane.test.mjs +237 -0
- package/bin/lean/limits.test.mjs +36 -1
- package/bin/lean/orchestrate.mjs +303 -41
- package/bin/lean/pitfalls.mjs +136 -0
- package/bin/lean/pitfalls.test.mjs +117 -0
- package/bin/lean/postmerge.mjs +85 -0
- package/bin/lean/postmerge.test.mjs +79 -0
- package/bin/lean/precheck.mjs +138 -13
- package/bin/lean/precheck.test.mjs +118 -1
- package/bin/lean/prefilter.mjs +220 -5
- package/bin/lean/prefilter.test.mjs +260 -4
- package/bin/lean/preflight.mjs +108 -3
- package/bin/lean/preflight.test.mjs +48 -1
- package/bin/lean/probe.mjs +297 -0
- package/bin/lean/probe.test.mjs +210 -0
- package/bin/lean/prompts.mjs +104 -13
- package/bin/lean/render.mjs +7 -3
- package/bin/lean/resume.mjs +67 -0
- package/bin/lean/resume.test.mjs +84 -0
- package/bin/lean/review-trigger.mjs +12 -0
- package/bin/lean/review-trigger.test.mjs +17 -1
- package/bin/lean/routing.mjs +74 -0
- package/bin/lean/routing.test.mjs +67 -0
- package/bin/lean/sched.mjs +16 -1
- package/bin/lean/sched.test.mjs +175 -1
- package/bin/lean/schedd.mjs +55 -9
- package/bin/lean/schedd.test.mjs +170 -3
- package/bin/lean/selfreview.mjs +37 -0
- package/bin/lean/selfreview.test.mjs +51 -0
- package/bin/lean/seqlease.mjs +40 -0
- package/bin/lean/seqlease.test.mjs +101 -0
- package/bin/lean/sequence.mjs +296 -13
- package/bin/lean/sequence.test.mjs +263 -2
- package/bin/lean/toolchain.mjs +60 -15
- package/bin/lean/toolchain.test.mjs +120 -3
- package/bin/lean/toolstats.mjs +72 -0
- package/bin/lean/toolstats.test.mjs +47 -0
- package/bin/lean/verifier.mjs +232 -0
- package/bin/lean/verifier.test.mjs +163 -0
- package/bin/lean/verify.mjs +72 -10
- package/bin/lean/verify.test.mjs +68 -3
- package/bin/review-staging.mjs +533 -0
- package/bin/runner.mjs +82 -13
- package/bin/stats/aggregate.mjs +87 -2
- package/bin/stats/efficiency.mjs +335 -0
- package/bin/stats/efficiency.test.mjs +266 -0
- package/bin/stats/github-source.mjs +8 -3
- package/bin/stats/index.mjs +91 -6
- package/bin/stats/local-source.mjs +145 -8
- package/bin/stats/query.mjs +181 -3
- package/bin/stats/render.mjs +22 -0
- package/bin/stats/stats.test.mjs +403 -8
- package/commands/orchestrate.md +3 -1
- package/commands/review-pr-agents/protocols.md +1 -1
- package/commands/review-pr-staging.md +84 -831
- package/commands/review-pr.md +1 -1
- package/commands/work-on/build/implement.md +1 -0
- package/commands/work-on/build/validate.md +11 -1
- package/docs/CONFIG.md +80 -5
- package/docs/OUTCOME-CODES.md +2 -1
- package/docs/STATS.md +66 -1
- package/docs/TELEMETRY.md +1 -1
- package/docs/eval/corpus/lightlane-v1.json +55 -0
- package/docs/eval/lightlane-bench.md +91 -0
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/scripts/bench-lightlane.mjs +286 -0
- package/scripts/ci-local/lib.mjs +20 -0
- package/scripts/ci-local/plan.mjs +2 -12
- package/scripts/ci-local/scoping.mjs +10 -1
- package/scripts/ci-local.test.sh +2 -0
- package/scripts/fixtures/apishape/other/go-change/base/x.go +3 -0
- package/scripts/fixtures/apishape/other/go-change/head/x.go +3 -0
- package/scripts/fixtures/apishape/python/generator-function/base/seq.py +3 -0
- package/scripts/fixtures/apishape/python/generator-function/head/seq.py +2 -0
- package/scripts/fixtures/apishape/python/ichunked/base/more.py +17 -0
- package/scripts/fixtures/apishape/python/ichunked/head/more.py +20 -0
- package/scripts/fixtures/apishape/python/private-edit/base/calc.py +6 -0
- package/scripts/fixtures/apishape/python/private-edit/head/calc.py +9 -0
- package/scripts/fixtures/apishape/python/removed/base/ops.py +6 -0
- package/scripts/fixtures/apishape/python/removed/head/ops.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/base/text.py +2 -0
- package/scripts/fixtures/apishape/python/renamed/head/text.py +2 -0
- package/scripts/fixtures/apishape/python/signature/base/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/signature/head/shapes.py +2 -0
- package/scripts/fixtures/apishape/python/sync-async/base/client.py +7 -0
- package/scripts/fixtures/apishape/python/sync-async/head/client.py +7 -0
- package/scripts/fixtures/apishape/ts/generator-function/base/seq.js +3 -0
- package/scripts/fixtures/apishape/ts/generator-function/head/seq.js +5 -0
- package/scripts/fixtures/apishape/ts/private-edit/base/calc.ts +7 -0
- package/scripts/fixtures/apishape/ts/private-edit/head/calc.ts +9 -0
- package/scripts/fixtures/apishape/ts/removed/base/ops.ts +7 -0
- package/scripts/fixtures/apishape/ts/removed/head/ops.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/base/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/renamed/head/text.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/base/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/signature/head/shapes.ts +3 -0
- package/scripts/fixtures/apishape/ts/sync-async/base/client.mjs +5 -0
- package/scripts/fixtures/apishape/ts/sync-async/head/client.mjs +5 -0
- package/scripts/forge-root.test.sh +2 -2
- package/scripts/publish-staging-sync.test.sh +62 -4
- package/scripts/review-finalize.sh +17 -6
- package/scripts/review-finalize.test.sh +47 -0
- package/scripts/review-pr-arguments.test.sh +13 -1
- package/scripts/review-pr-delta-wiring.test.sh +2 -2
- package/scripts/review-pr-gate2.test.sh +6 -14
- package/scripts/review-pr-staging-finding-filter.test.sh +4 -4
- package/scripts/review-pr-staging-scope.test.sh +39 -33
- package/scripts/review-prepare.sh +73 -6
- package/scripts/review-prepare.test.sh +85 -0
- package/scripts/review-staging-post.sh +293 -0
- package/scripts/review-staging-post.test.sh +143 -0
- package/scripts/review-staging-pre.sh +401 -0
- package/scripts/review-staging-pre.test.sh +150 -0
- package/scripts/symbol-index.mjs +26 -7
- package/scripts/symbol-index.py +55 -9
- package/scripts/trusted-comments.test.sh +1 -1
package/bin/stats/stats.test.mjs
CHANGED
|
@@ -8,13 +8,13 @@ import { tmpdir } from "node:os";
|
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import { spawnSync } from "node:child_process";
|
|
11
|
-
import { aggregate, compareVersions, percentile } from "./aggregate.mjs";
|
|
11
|
+
import { aggregate, compareVersions, computeAlerts, DEFAULT_ALERTS, percentile } from "./aggregate.mjs";
|
|
12
12
|
import { cacheDir } from "./cache.mjs";
|
|
13
13
|
import { fetchGithubRecords, parseTelemetryComments, runsFromRecords, toModel } from "./github-source.mjs";
|
|
14
|
-
import { buildReport, makeProgressPrinter, parseArgs, parseSince, runStats, sinceToTimestamp } from "./index.mjs";
|
|
15
|
-
import { readLocalRuns, summariseLog } from "./local-source.mjs";
|
|
16
|
-
import { renderCompact, renderJson, renderMarkdown } from "./render.mjs";
|
|
17
|
-
import { edgeQuery, parseEdge, verdictMix, contextPackAb, renderContextAb } from "./query.mjs";
|
|
14
|
+
import { buildReport, makeProgressPrinter, parseArgs, parseSince, readAlertConfig, runStats, sinceToTimestamp } from "./index.mjs";
|
|
15
|
+
import { lanesOfLog, readLanes, readLocalLaneRuns, readLocalRuns, summariseLog } from "./local-source.mjs";
|
|
16
|
+
import { renderAlerts, renderCompact, renderJson, renderMarkdown } from "./render.mjs";
|
|
17
|
+
import { edgeQuery, parseEdge, verdictMix, contextPackAb, renderContextAb, builderTierReport, renderTiers, exploreAb, renderExploreAb, laneProfileReport, renderLaneProfiles } from "./query.mjs";
|
|
18
18
|
|
|
19
19
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
const fx = (f) => join(here, "fixtures", f);
|
|
@@ -538,21 +538,23 @@ describe("context-pack A/B over run logs (forge#4275)", () => {
|
|
|
538
538
|
|
|
539
539
|
it("summariseLog carries mode, lane type, builder turns, cache tokens, cost and first-pass", () => {
|
|
540
540
|
const s = summariseLog(log("on", "child"));
|
|
541
|
-
assert.deepEqual(s.contextPack, { mode: "on", laneType: "child" });
|
|
541
|
+
assert.deepEqual(s.contextPack, { mode: "on", laneType: "child", ab: false });
|
|
542
542
|
assert.deepEqual([s.builderTurns, s.cacheTokens, s.costUsd, s.firstPass], [10, 2130, 1.1, true]);
|
|
543
543
|
assert.equal(summariseLog(log("on", "child", { builds: 4 })).firstPass, false);
|
|
544
544
|
assert.equal(summariseLog(log("on", "child", { reason: "needs-human" })).firstPass, false);
|
|
545
545
|
const fixed = log("on", "child"); fixed.splice(-1, 0, { event: "CI_FIX_ROUND", trigger: "build", round: 1 });
|
|
546
546
|
assert.equal(summariseLog(fixed).firstPass, false);
|
|
547
547
|
assert.equal(summariseLog([{ event: "RUN_START", issue: 3 }, { event: "RUN_TERMINAL", reason: "merged" }]).contextPack.mode, "off");
|
|
548
|
+
assert.equal(summariseLog([{ event: "RUN_START", issue: 3 }, { event: "RUN_TERMINAL", reason: "merged" }]).size, null);
|
|
549
|
+
assert.equal(summariseLog([{ event: "RUN_START", issue: 3 }, { event: "RUN_TERMINAL", reason: "merged", turns: { size: "L", budget: 100, build: 9 } }]).size, "L"); // #4409
|
|
548
550
|
});
|
|
549
551
|
|
|
550
552
|
it("reports per lane type and mode, never one blended average", () => {
|
|
551
553
|
const runs = [log("on", "first"), log("on", "first", { builds: 4, reviews: 2 }), log("off", "first"), log("shadow", "rerun"),
|
|
552
554
|
[{ event: "RUN_START", issue: 2 }, { event: "RUN_TERMINAL", reason: "merged" }]].map((l) => summariseLog(l));
|
|
553
555
|
const ab = contextPackAb(runs);
|
|
554
|
-
assert.deepEqual(Object.keys(ab), ["rerun", "child", "first"]);
|
|
555
|
-
assert.deepEqual(ab.first.on, { n: 2, first_pass_yield: 0.5, builder_turns: 15, cache_tokens: 3195, cost_usd: 1.65 });
|
|
556
|
+
assert.deepEqual(Object.keys(ab), ["rerun", "child", "first", "recovery"]);
|
|
557
|
+
assert.deepEqual(ab.first.on, { n: 2, first_pass_yield: 0.5, builder_turns: 15, cache_tokens: 3195, cost_usd: 1.65, merged: 2, turns_per_merge: 15, cache_read_per_merge: 3150, rework_rate: 0 });
|
|
556
558
|
assert.equal(ab.first.off.n, 1); assert.equal(ab.first.off.first_pass_yield, 1);
|
|
557
559
|
assert.equal(ab.rerun.shadow.n, 1); assert.equal(ab.child.on.n, 0); assert.equal(ab.child.on.cost_usd, null);
|
|
558
560
|
assert.match(renderContextAb(ab), /\| first \| on \| 2 \| 0\.5 \| 15 \| 3195 \| 1\.65 \|/);
|
|
@@ -564,3 +566,396 @@ describe("context-pack A/B over run logs (forge#4275)", () => {
|
|
|
564
566
|
assert.equal(rep.query.context_ab.first.on.n, 1);
|
|
565
567
|
});
|
|
566
568
|
});
|
|
569
|
+
|
|
570
|
+
describe("explorer A/B over run logs (forge#4418)", () => {
|
|
571
|
+
const use = (phase, turns, cr) => ({ event: "PHASE_ATTEMPT_USAGE", phase, turns, cost_usd: 0.5, usage: { cache_read_input_tokens: cr, cache_creation_input_tokens: 10, output_tokens: 1 } });
|
|
572
|
+
const slice = (issue, explore, { size = "M", turns = 5, cr = 1000, reason = "merged", triggers = ["plan", "build"], review = true } = {}) => [
|
|
573
|
+
{ event: "RUN_START", issue, repo: "o/r", run: `r${issue}${explore}` },
|
|
574
|
+
...triggers.map((t) => ({ ...use("implement", turns, cr), trigger: t })), ...(review ? [use("review", 2, 100)] : []),
|
|
575
|
+
{ event: "RUN_TERMINAL", reason, code: "x", ...(explore ? { explore } : {}), ...(size ? { turns: { size, budget: 100, build: 9 } } : {}) },
|
|
576
|
+
];
|
|
577
|
+
|
|
578
|
+
it("summariseLog carries the explore tag and the plan + build cache-read", () => {
|
|
579
|
+
const on = summariseLog(slice(1, "on"));
|
|
580
|
+
assert.deepEqual([on.explore, on.builderCacheRead, on.builderTurns, on.cacheTokens], ["on", 2000, 10, 2130]);
|
|
581
|
+
assert.equal(summariseLog(slice(1, "off")).explore, "off");
|
|
582
|
+
assert.equal(summariseLog(slice(1, null)).explore, null);
|
|
583
|
+
assert.equal(summariseLog(slice(1, "maybe")).explore, null);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("readLocalLaneRuns keeps every RUN_START slice of a log", () => {
|
|
587
|
+
const dir = tmpDir("explore-ab-");
|
|
588
|
+
try {
|
|
589
|
+
writeFileSync(join(dir, "o__r__7.jsonl"), [...slice(7, "on"), ...slice(7, "off", { reason: "needs-human" })].map((e) => JSON.stringify(e)).join("\n"));
|
|
590
|
+
const runs = readLocalLaneRuns(dir);
|
|
591
|
+
assert.deepEqual(runs.map((r) => [r.issue, r.explore, r.reason]), [[7, "on", "merged"], [7, "off", "needs-human"]]);
|
|
592
|
+
assert.equal(readLocalRuns(dir).length, 1);
|
|
593
|
+
assert.deepEqual(readLocalLaneRuns(join(dir, "missing")), []);
|
|
594
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
it("reports per size and mode, pairs same issues and leaves untagged runs out", () => {
|
|
598
|
+
const runs = [slice(1, "on", { size: "L", turns: 3, cr: 500 }), slice(1, "off", { size: "L", turns: 10, cr: 4000 }),
|
|
599
|
+
slice(2, "on", { size: "L", turns: 4, cr: 600, triggers: ["plan", "build", "conformance"] }), slice(3, "off", { size: "S", reason: "needs-human", review: false }),
|
|
600
|
+
slice(4, null), slice(5, "on", { size: null })].map((l) => summariseLog(l));
|
|
601
|
+
const ab = exploreAb(runs);
|
|
602
|
+
assert.deepEqual(Object.keys(ab.by_size), ["S", "M", "L", "unsized"]);
|
|
603
|
+
assert.deepEqual(ab.by_size.L.on, { n: 2, builder_turns: 9, builder_cache_read: 1400, merge_rate: 1, conformance_rate: 0.5, reviewed: 2 });
|
|
604
|
+
assert.deepEqual(ab.by_size.L.off, { n: 1, builder_turns: 20, builder_cache_read: 8000, merge_rate: 1, conformance_rate: 1, reviewed: 1 });
|
|
605
|
+
assert.deepEqual([ab.by_size.S.off.merge_rate, ab.by_size.S.off.conformance_rate, ab.by_size.M.on.n, ab.by_size.M.on.builder_turns], [0, null, 0, null]);
|
|
606
|
+
assert.equal(ab.by_size.unsized.on.n, 1);
|
|
607
|
+
assert.equal(ab.untagged, 1);
|
|
608
|
+
assert.equal(ab.paired.issues, 1);
|
|
609
|
+
assert.deepEqual([ab.paired.on.n, ab.paired.on.builder_cache_read, ab.paired.off.n, ab.paired.off.builder_cache_read], [1, 1000, 1, 8000]);
|
|
610
|
+
const md = renderExploreAb(ab);
|
|
611
|
+
assert.match(md, /\| L \| on \| 2 \| 9 \| 1400 \| 1 \| 0\.5 \(2\) \|/);
|
|
612
|
+
assert.match(md, /\| paired \| off \| 1 \| 20 \| 8000 \| 1 \| 1 \(1\) \|/);
|
|
613
|
+
assert.match(md, /Same issues in both modes: 1/);
|
|
614
|
+
assert.match(md, /1 finished run\(s\) without an explore tag/);
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
it("--explore-ab reads run logs only (no GitHub crawl)", async () => {
|
|
618
|
+
assert.equal(parseArgs(["--explore-ab"]).exploreAb, true);
|
|
619
|
+
assert.equal(parseArgs([]).exploreAb, false);
|
|
620
|
+
const rep = await buildReport({ repo: "o/r", exploreAb: true }, { localRuns: [summariseLog(slice(1, "on")), { ...summariseLog(slice(2, "on")), repo: "x/y" }], releaseTimes: [], gh: async () => { throw new Error("no network"); } });
|
|
621
|
+
assert.equal(rep.query.explore_ab.by_size.M.on.n, 1);
|
|
622
|
+
assert.equal(rep.sources.local.runs, 1);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
describe("context-pack A/B per merge, rework and recovery vs first (#4444)", () => {
|
|
627
|
+
const use = (trigger, turns, cr, out) => ({ event: "PHASE_ATTEMPT_USAGE", phase: "implement", trigger, turns, cost_usd: 0.5, usage: { cache_read_input_tokens: cr, cache_creation_input_tokens: 10, output_tokens: out } });
|
|
628
|
+
const slice = (issue, lt, { reason = "merged", mode = "on", ab = false, calls = [["plan", 2, 100, 10], ["build", 10, 1000, 400]], extra = [] } = {}) => [
|
|
629
|
+
{ event: "RUN_START", issue, repo: "o/r", run: `r${issue}${lt}`, ts: lt === "rerun" ? "2026-10-02T00:00:00Z" : "2026-10-01T00:00:00Z" },
|
|
630
|
+
...calls.map((c) => use(...c)), ...extra,
|
|
631
|
+
{ event: "RUN_TERMINAL", reason, code: "x", context_pack: mode, lane_type: lt, ...(ab ? { context_ab: true } : {}) },
|
|
632
|
+
];
|
|
633
|
+
|
|
634
|
+
it("summariseLog: cache-read alone, the implement attempt without the plan call, the ab tag and the log's first-run baseline", () => {
|
|
635
|
+
const s = summariseLog([...slice(5, "first"), ...slice(5, "rerun", { ab: true, calls: [["recover", 3, 50, 100]] })]);
|
|
636
|
+
assert.equal(s.cacheReadTokens, 50);
|
|
637
|
+
assert.deepEqual(s.implementAttempt, { turns: 3, output_tokens: 100 });
|
|
638
|
+
assert.deepEqual(s.firstRunAttempt, { turns: 10, output_tokens: 400 });
|
|
639
|
+
assert.equal(s.contextPack.ab, true);
|
|
640
|
+
assert.equal(summariseLog(slice(5, "first")).firstRunAttempt, null);
|
|
641
|
+
assert.deepEqual(summariseLog(slice(6, "first", { calls: [] })).implementAttempt, null);
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it("per-merge turns and cache-read, rework rate and the same-issue recovery ratio", () => {
|
|
645
|
+
const runs = [summariseLog(slice(1, "first")), summariseLog(slice(2, "first", { reason: "needs-human" })),
|
|
646
|
+
summariseLog(slice(3, "first", { extra: [{ event: "PHASE_COMMIT", phase: "remediate" }] })),
|
|
647
|
+
summariseLog([...slice(4, "first"), ...slice(4, "rerun", { calls: [["recover", 5, 0, 100]] })]),
|
|
648
|
+
summariseLog(slice(1, "rerun", { calls: [["recover", 2, 0, 200]] }))];
|
|
649
|
+
const ab = contextPackAb(runs);
|
|
650
|
+
// three runs: 12 builder turns each, 1100 cache-read each, two merged
|
|
651
|
+
assert.deepEqual([ab.first.on.merged, ab.first.on.turns_per_merge, ab.first.on.cache_read_per_merge, ab.first.on.rework_rate], [2, 18, 1650, 0.3333]);
|
|
652
|
+
assert.equal(ab.rerun.on.rework_rate, 1);
|
|
653
|
+
assert.equal(ab.child.on.turns_per_merge, null);
|
|
654
|
+
// issue 4: 5/10 turns, 100/400 output (own log); issue 1: 2/10 turns, 200/400 output (earlier first run in `runs`)
|
|
655
|
+
assert.deepEqual(ab.recovery, { pairs: 2, mean_turn_ratio: 0.35, mean_output_ratio: 0.375 });
|
|
656
|
+
const md = renderContextAb(ab);
|
|
657
|
+
assert.match(md, /\| Merged \| Turns\/merge \| Cache-read\/merge \| Rework rate \|/);
|
|
658
|
+
assert.match(md, /\| first \| on \| 3 \| .* \| 2 \| 18 \| 1650 \| 0\.3333 \|/);
|
|
659
|
+
assert.match(md, /Recovery vs first run \(same issue\): 2 pair\(s\), implement turns ratio 0\.35, output tokens ratio 0\.375/);
|
|
660
|
+
assert.deepEqual(contextPackAb([]).recovery, { pairs: 0, mean_turn_ratio: null, mean_output_ratio: null });
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
describe("builder model tiers over run logs (forge#4394)", () => {
|
|
665
|
+
const use = (phase, trigger, usd = 1) => ({ event: "PHASE_ATTEMPT_USAGE", phase, trigger, cost_usd: usd, usage: { cache_read_input_tokens: 1, output_tokens: 1 } });
|
|
666
|
+
const slice = (issue, { tier = "mid", cls = "small", escalated = false, reason = "merged", triggers = ["plan", "build"], review = true, shadow } = {}) => [
|
|
667
|
+
{ event: "RUN_START", issue, repo: "o/r", run: `r${issue}` },
|
|
668
|
+
...triggers.map((t) => use("implement", t)), ...(review ? [use("review", "review", 0.5)] : []),
|
|
669
|
+
{ event: "RUN_TERMINAL", reason, code: "x", ...(shadow ? { shadow_would: shadow } : {}), builder_tier: tier, builder_class: cls, builder_escalated: escalated, builder_model: tier === "mid" ? "claude-sonnet-5-5" : "default" },
|
|
670
|
+
];
|
|
671
|
+
|
|
672
|
+
it("summariseLog reads builderRoute from RUN_TERMINAL, else the BUILDER_ROUTE/BUILDER_ESCALATED events; null before routing", () => {
|
|
673
|
+
const s = summariseLog(slice(1, { escalated: true, triggers: ["plan", "build", "conformance"], shadow: "conformance.scope" }));
|
|
674
|
+
assert.deepEqual(s.builderRoute, { tier: "mid", class: "small", escalated: true, model: "claude-sonnet-5-5" });
|
|
675
|
+
assert.equal(s.conformanceRound, true); assert.equal(s.shadowWould, "conformance.scope"); assert.equal(s.reachedReview, true);
|
|
676
|
+
const evs = [{ event: "RUN_START", issue: 2 }, { event: "BUILDER_ROUTE", tier: "mid", class: "docs-config-test", model: "m" }, { event: "BUILDER_ESCALATED", from: "m", to: "default", trigger: "ci_red" }, { event: "RUN_TERMINAL", reason: "merged" }];
|
|
677
|
+
assert.deepEqual(summariseLog(evs).builderRoute, { tier: "mid", class: "docs-config-test", escalated: true, model: "m" });
|
|
678
|
+
const old = summariseLog([{ event: "RUN_START", issue: 3 }, { event: "RUN_TERMINAL", reason: "merged" }]);
|
|
679
|
+
assert.equal(old.builderRoute, null); assert.equal(old.conformanceRound, false); assert.equal(old.shadowWould, null);
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it("reports merge, conformance and post-merge-rework rates per tier and class; unrouted runs are left out", () => {
|
|
683
|
+
const reworked = [...slice(5, { tier: "top", cls: "large" }), ...slice(5, { tier: "top", cls: "large", reason: "needs-human" })]; // a later run of a merged issue
|
|
684
|
+
const runs = [slice(1), slice(2, { triggers: ["plan", "build", "review"] }), slice(3, { reason: "needs-human", review: false }),
|
|
685
|
+
slice(4, { escalated: true, triggers: ["plan", "build", "precheck"] }), reworked, [{ event: "RUN_START", issue: 9 }, { event: "RUN_TERMINAL", reason: "merged" }]].map((l) => summariseLog(l));
|
|
686
|
+
const rep = builderTierReport(runs);
|
|
687
|
+
assert.equal(rep.unrouted, 1);
|
|
688
|
+
assert.deepEqual(rep.rows.map((g) => [g.tier, g.class]), [["mid", "small"], ["mid->top", "small"], ["top", "large"]]);
|
|
689
|
+
const [mid, esc, top] = rep.rows;
|
|
690
|
+
assert.deepEqual([mid.n, mid.merge_rate, mid.reviewed, mid.conformance_rate, mid.merges, mid.post_merge_rework_rate, mid.first_pass_yield], [3, 0.6667, 2, 0.5, 2, 0, 0.3333]);
|
|
691
|
+
assert.deepEqual([esc.n, esc.merge_rate, esc.conformance_rate], [1, 1, 1]);
|
|
692
|
+
assert.deepEqual([top.n, top.merge_rate, top.merges, top.post_merge_rework_rate], [1, 0, 1, 1]);
|
|
693
|
+
const md = renderTiers(rep);
|
|
694
|
+
assert.match(md, /\| mid \| small \| 3 \| 0\.6667 \| 0\.5 \(2\) \| 0 \(2\) \| 0\.3333 \|/);
|
|
695
|
+
assert.match(md, /\| top \| large \| 1 \| 0 \| .* \| 1 \(1\) \|/);
|
|
696
|
+
assert.match(md, /1 finished run\(s\) without a builder route/);
|
|
697
|
+
assert.match(renderTiers(builderTierReport([])), /\| none \|/);
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
it("--tiers reads run logs only (no GitHub crawl); other reports carry no tiers", async () => {
|
|
701
|
+
assert.equal(parseArgs(["--tiers"]).tiers, true); assert.equal(parseArgs([]).tiers, false);
|
|
702
|
+
const rep = await buildReport({ repo: "o/r", tiers: true }, { localRuns: [summariseLog(slice(1))], releaseTimes: [], gh: async () => { throw new Error("no network"); } });
|
|
703
|
+
assert.equal(rep.query.tiers.rows[0].n, 1);
|
|
704
|
+
});
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
describe("lane profile report (#4436)", () => {
|
|
708
|
+
const use = (phase, profile, baseline, trigger = "build") => ({ event: "PHASE_ATTEMPT_USAGE", phase, trigger, cost_usd: 1, usage: { cache_read_input_tokens: 1, output_tokens: 1 },
|
|
709
|
+
...(profile ? { lane_profile: profile } : {}), ...(baseline != null ? { baseline_tokens: baseline } : {}) });
|
|
710
|
+
const slice = (issue, profile, { impl = [40000], review = 20000, shadow, triggers } = {}) => [
|
|
711
|
+
{ event: "RUN_START", issue, repo: "o/r", run: `r${issue}${profile}` },
|
|
712
|
+
...impl.map((b, i) => use("implement", profile, b, triggers?.[i])), ...(review != null ? [use("review", profile, review, "review")] : []),
|
|
713
|
+
{ event: "RUN_TERMINAL", reason: "merged", ...(shadow ? { shadow_would: shadow } : {}) },
|
|
714
|
+
];
|
|
715
|
+
|
|
716
|
+
it("summariseLog keeps the slice's lane profile and per-phase baselines; resumed sessions add none", () => {
|
|
717
|
+
const s = summariseLog(slice(1, "lane", { impl: [30000, null] }));
|
|
718
|
+
assert.equal(s.laneProfile, "lane");
|
|
719
|
+
assert.deepEqual(s.baselines, { implement: [30000], review: [20000] });
|
|
720
|
+
const mixed = summariseLog([{ event: "RUN_START", issue: 2 }, use("implement", "memory-off", 1), use("review", "lane", 2), { event: "RUN_TERMINAL", reason: "merged" }]);
|
|
721
|
+
assert.equal(mixed.laneProfile, "mixed");
|
|
722
|
+
const old = summariseLog([{ event: "RUN_START", issue: 3 }, use("implement", null, null), { event: "RUN_TERMINAL", reason: "merged" }]);
|
|
723
|
+
assert.equal(old.laneProfile, null); assert.deepEqual(old.baselines, {});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
it("readLocalRuns keeps one summary per log by default and every RUN_START slice with allSlices", () => {
|
|
727
|
+
const dir = tmpDir("lane-profile-");
|
|
728
|
+
try {
|
|
729
|
+
writeFileSync(join(dir, "7.jsonl"), [...slice(7, "memory-off", { impl: [60000] }), ...slice(7, "lane", { impl: [25000] })].map((e) => JSON.stringify(e)).join("\n"));
|
|
730
|
+
const last = readLocalRuns(dir);
|
|
731
|
+
assert.equal(last.length, 1); assert.equal(last[0].laneProfile, "lane");
|
|
732
|
+
const all = readLocalRuns(dir, { allSlices: true });
|
|
733
|
+
assert.deepEqual(all.map((r) => [r.issue, r.laneProfile, r.baselines.implement[0]]), [[7, "memory-off", 60000], [7, "lane", 25000]]);
|
|
734
|
+
} finally { rmSync(dir, { recursive: true, force: true }); }
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it("compares baseline and conformance per profile on a pinned issue set", () => {
|
|
738
|
+
const runs = [
|
|
739
|
+
slice(1, "memory-off", { impl: [60000, 70000] }), slice(2, "memory-off", { impl: [50000], shadow: "conformance.scope" }),
|
|
740
|
+
slice(1, "lane", { impl: [20000, 30000], review: 10000 }), slice(2, "lane", { impl: [24000], review: 12000, triggers: ["conformance"] }),
|
|
741
|
+
slice(3, "lane", { impl: [99999] }), [{ event: "RUN_START", issue: 4 }, use("implement", null, null), { event: "RUN_TERMINAL", reason: "merged" }],
|
|
742
|
+
[{ event: "RUN_START", issue: 5 }, use("implement", "lane", 1)], // unfinished: left out
|
|
743
|
+
].map((l) => summariseLog(l));
|
|
744
|
+
const all = laneProfileReport(runs);
|
|
745
|
+
assert.deepEqual(all.profiles.map((p) => [p.profile, p.n]), [["memory-off", 2], ["lane", 3], ["unknown", 1]]);
|
|
746
|
+
assert.deepEqual(all.issues_in_all, []);
|
|
747
|
+
const rep = laneProfileReport(runs, { issues: [1, 2] });
|
|
748
|
+
assert.deepEqual(rep.profiles.map((p) => p.profile), ["memory-off", "lane"]);
|
|
749
|
+
const [before, after] = rep.profiles;
|
|
750
|
+
assert.deepEqual(before.phases, [{ phase: "implement", sessions: 3, median: 60000, mean: 60000 }, { phase: "review", sessions: 2, median: 20000, mean: 20000 }]);
|
|
751
|
+
assert.deepEqual(after.phases[0], { phase: "implement", sessions: 3, median: 24000, mean: 24667 });
|
|
752
|
+
assert.deepEqual([before.reviewed, before.conformance_rate, before.shadow_blocks], [2, 0.5, 1]);
|
|
753
|
+
assert.deepEqual([after.reviewed, after.conformance_rate, after.shadow_blocks], [2, 0.5, 0]);
|
|
754
|
+
assert.deepEqual(rep.issues_in_all, [1, 2]);
|
|
755
|
+
const md = renderLaneProfiles(rep);
|
|
756
|
+
assert.match(md, /\| lane \| implement \| 3 \| 24000 \| 24667 \|/);
|
|
757
|
+
assert.match(md, /\| memory-off \| 2 \| 0\.5 \(2\) \| 1 \|/);
|
|
758
|
+
assert.match(md, /every profile: #1, #2/);
|
|
759
|
+
assert.match(renderLaneProfiles(laneProfileReport([])), /\| none \|/);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it("--lane-profile reads run logs only (no GitHub crawl); --issues takes a comma list", async () => {
|
|
763
|
+
assert.equal(parseArgs(["--lane-profile"]).laneProfile, true); assert.equal(parseArgs([]).laneProfile, false);
|
|
764
|
+
assert.deepEqual(parseArgs(["--issues", "4,12"]).issues, [4, 12]); assert.deepEqual(parseArgs(["--issues=7"]).issues, [7]);
|
|
765
|
+
assert.throws(() => parseArgs(["--issues", "4,x"]), /--issues must be a comma list/);
|
|
766
|
+
const rep = await buildReport({ repo: "o/r", laneProfile: true, issues: [1] }, { localRuns: [summariseLog(slice(1, "lane")), summariseLog(slice(2, "lane"))],
|
|
767
|
+
releaseTimes: [], gh: async () => { throw new Error("no network"); } });
|
|
768
|
+
assert.deepEqual(rep.query.lane_profile.profiles.map((p) => [p.profile, p.n]), [["lane", 1]]);
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
describe("rework by trigger and first-pass rate (#4362)", () => {
|
|
773
|
+
const use = (attempt, trigger, usd, phase = "implement") => ({ event: "PHASE_ATTEMPT_USAGE", phase, attempt, ...(trigger ? { trigger } : {}), cost_usd: usd, usage: { cache_read_input_tokens: 10, output_tokens: 1 } });
|
|
774
|
+
const log = (issue, attempts) => [{ event: "RUN_START", issue, repo: "o/r", run: `r${issue}`, version: "1.30.0", ts: "2026-10-20T00:00:00Z" }, ...attempts,
|
|
775
|
+
{ event: "RUN_TERMINAL", reason: "merged", ts: "2026-10-20T01:00:00Z" }];
|
|
776
|
+
const runs = [
|
|
777
|
+
log(1, [use(1, "plan", 1), use(2, "build", 3)]),
|
|
778
|
+
log(2, [use(1, "plan", 1), use(2, "build", 3), use(3, "precheck", 2), use(4, "review", 4), use(1, "review", 0.2, "review")]),
|
|
779
|
+
log(3, [use(1, "plan", 1), use(2, "build", 3), use(3, "timeout_restart", 1), use(4, "recovery", 1)]),
|
|
780
|
+
log(4, [use(1, null, 1), use(2, null, 3), use(3, null, 5)]), // written before the field existed
|
|
781
|
+
log(5, [use(1, "plan", 1)]), // closed at the plan: never reached the build
|
|
782
|
+
].map((l) => summariseLog(l));
|
|
783
|
+
|
|
784
|
+
it("summariseLog keeps builder attempts in order with their trigger (null when untagged)", () => {
|
|
785
|
+
assert.deepEqual(runs[1].builderAttempts.map((a) => a.trigger), ["plan", "build", "precheck", "review"]);
|
|
786
|
+
assert.deepEqual(runs[3].builderAttempts.map((a) => [a.attempt, a.trigger, a.cost_usd]), [[1, null, 1], [2, null, 3], [3, null, 5]]);
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
it("aggregate counts rework by trigger (untagged as unknown, never dropped) and the first-pass rate with counts", () => {
|
|
790
|
+
const g = aggregate({ runs }).groups[0];
|
|
791
|
+
assert.deepEqual(g.rework.byTrigger, { precheck: { n: 1, usd: 2 }, review: { n: 1, usd: 4 }, unknown: { n: 3, usd: 9 } });
|
|
792
|
+
assert.deepEqual([g.rework.n, g.rework.usd], [5, 15]);
|
|
793
|
+
assert.deepEqual(g.rework.attempts.build, { n: 3, usd: 9 });
|
|
794
|
+
assert.deepEqual(g.firstPass, { rate: 2 / 3, firstPass: 2, n: 3, unknown: 1 });
|
|
795
|
+
const md = renderMarkdown(aggregate({ runs }));
|
|
796
|
+
assert.match(md, /first-pass rate \| 67% \(2\/3 lanes that reached the build, 1 unknown\)/);
|
|
797
|
+
assert.match(md, /### Rework by trigger/);
|
|
798
|
+
assert.match(md, /\| unknown \| 3 \| \$9\.00 \|/);
|
|
799
|
+
assert.match(md, /\| total \| 5 \| \$15\.00 \|/);
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it("GitHub phase_attempt records carry the trigger into builderAttempts, ordered by attempt", () => {
|
|
803
|
+
const rec = (attempt, trigger, ts) => ({ event: "phase_attempt", repo: "o/r", issue: 9, run: "r_9", phase: "implement", attempt, ...(trigger ? { trigger } : {}), cost_usd: 1, output_tokens: 1, ts });
|
|
804
|
+
const [run] = runsFromRecords([rec(3, "ci_red", "t3"), rec(1, "plan", "t1"), rec(2, "build", "t2"), rec(2, "timeout_restart", "t2b"), rec(4, null, "t4"),
|
|
805
|
+
{ event: "run_terminal", repo: "o/r", issue: 9, run: "r_9", reason: "merged", ts: "t5" }]);
|
|
806
|
+
assert.deepEqual(run.builderAttempts.map((a) => [a.attempt, a.trigger]), [[1, "plan"], [2, "build"], [2, "timeout_restart"], [3, "ci_red"], [4, null]]);
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
describe("per-attempt cost deltas (#4407)", () => {
|
|
811
|
+
const use = (attempt, usd, delta, phase = "implement") => ({ event: "PHASE_ATTEMPT_USAGE", phase, attempt, trigger: attempt === 1 ? "plan" : "build", cost_usd: usd,
|
|
812
|
+
...(delta === undefined ? {} : { cost_usd_delta: delta, cost_usd_session: usd }), usage: { cache_read_input_tokens: 10, output_tokens: 1 } });
|
|
813
|
+
const log = (attempts) => [{ event: "RUN_START", issue: 1, repo: "o/r", run: "r1", version: "1.30.0", ts: "2026-10-20T00:00:00Z" }, ...attempts,
|
|
814
|
+
{ event: "RUN_TERMINAL", reason: "merged", ts: "2026-10-20T01:00:00Z" }];
|
|
815
|
+
|
|
816
|
+
it("a local run sums cost_usd_delta when present, else cost_usd (old logs)", () => {
|
|
817
|
+
const r = summariseLog(log([use(1, 2, 2), use(2, 5, 3), use(1, 0.5, 0.5, "review")]));
|
|
818
|
+
assert.equal(r.costUsd, 5.5);
|
|
819
|
+
assert.equal(r.phases.implement.usd, 5);
|
|
820
|
+
assert.deepEqual(r.builderAttempts.map((a) => a.cost_usd), [2, 3]);
|
|
821
|
+
const old = summariseLog(log([use(1, 2), use(2, 5)]));
|
|
822
|
+
assert.equal(old.costUsd, 7); assert.deepEqual(old.builderAttempts.map((a) => a.cost_usd), [2, 5]);
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
it("GitHub phase_attempt records sum cost_usd_delta when present, else cost_usd", () => {
|
|
826
|
+
const rec = (attempt, usd, delta) => ({ event: "phase_attempt", repo: "o/r", issue: 9, run: "r_9", phase: "implement", attempt, cost_usd: usd, ...(delta === undefined ? {} : { cost_usd_delta: delta }), output_tokens: 1, ts: `t${attempt}` });
|
|
827
|
+
const [run] = runsFromRecords([rec(1, 2, 2), rec(2, 5, 3), rec(3, 4), { event: "run_terminal", repo: "o/r", issue: 9, run: "r_9", reason: "merged", ts: "t9" }]);
|
|
828
|
+
assert.equal(run.phases.implement.usd, 9);
|
|
829
|
+
assert.deepEqual(run.builderAttempts.map((a) => a.cost_usd), [2, 3, 4]);
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
describe("first-pass rate with and without matching pitfalls (#4375)", () => {
|
|
834
|
+
const use = (attempt, trigger) => ({ event: "PHASE_ATTEMPT_USAGE", phase: "implement", attempt, trigger, cost_usd: 1, usage: { cache_read_input_tokens: 10, output_tokens: 1 } });
|
|
835
|
+
const log = (issue, attempts, { term = {}, pack = null } = {}) => [{ event: "RUN_START", issue, repo: "o/r", run: `r${issue}`, version: "1.30.0", ts: "2026-10-20T00:00:00Z" },
|
|
836
|
+
...(pack ? [{ event: "CONTEXT_PACK", mode: "shadow", pitfalls: pack }] : []), ...attempts, { event: "RUN_TERMINAL", reason: "merged", ts: "2026-10-20T01:00:00Z", ...term }];
|
|
837
|
+
const clean = [use(1, "plan"), use(2, "build")], rework = [...clean, use(3, "precheck")];
|
|
838
|
+
|
|
839
|
+
it("summariseLog reads pitfalls_matched from RUN_TERMINAL, else the CONTEXT_PACK pitfalls, else null", () => {
|
|
840
|
+
assert.equal(summariseLog(log(1, clean, { term: { pitfalls_matched: 2 } })).pitfallsMatched, 2);
|
|
841
|
+
assert.equal(summariseLog(log(1, clean, { pack: [{ sig: "a", match: "files" }, { sig: "b", match: "repo" }] })).pitfallsMatched, 1);
|
|
842
|
+
assert.equal(summariseLog(log(1, clean)).pitfallsMatched, null, "older logs: unknown");
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it("aggregate splits the first-pass rate by matching pitfalls and leaves unknown runs out; render prints both rows", () => {
|
|
846
|
+
const runs = [
|
|
847
|
+
log(1, clean, { term: { pitfalls_matched: 1 } }), log(2, clean, { term: { pitfalls_matched: 3 } }), log(3, rework, { term: { pitfalls_matched: 1 } }),
|
|
848
|
+
log(4, rework, { term: { pitfalls_matched: 0 } }), log(5, clean, { term: { pitfalls_matched: 0 } }), log(6, rework, { term: { pitfalls_matched: 0 } }),
|
|
849
|
+
log(7, clean), // no pitfall field: unknown
|
|
850
|
+
].map((l) => summariseLog(l));
|
|
851
|
+
const g = aggregate({ runs }).groups[0];
|
|
852
|
+
assert.deepEqual(g.firstPassByPitfalls, { with: { rate: 2 / 3, firstPass: 2, n: 3 }, without: { rate: 1 / 3, firstPass: 1, n: 3 } });
|
|
853
|
+
assert.equal(g.firstPass.n, 7, "the overall rate still counts every lane");
|
|
854
|
+
const md = renderMarkdown(aggregate({ runs }));
|
|
855
|
+
assert.match(md, /first-pass with matching pitfalls \| 67% \(2\/3\)/);
|
|
856
|
+
assert.match(md, /first-pass without matching pitfalls \| 33% \(1\/3\)/);
|
|
857
|
+
const none = renderMarkdown(aggregate({ runs: [log(8, clean)].map((l) => summariseLog(l)) }));
|
|
858
|
+
assert.ok(!/matching pitfalls/.test(none), "no pitfall data: no rows");
|
|
859
|
+
});
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
describe("fail-soft alerts (#4354)", () => {
|
|
863
|
+
const ts = (i) => new Date(Date.UTC(2026, 9, 1) + i * 60000).toISOString();
|
|
864
|
+
// One lane per call; `extra` are the events after RUN_START.
|
|
865
|
+
const lane = (i, extra = [], { repo = "o/r", code = "completed", reason = "merged" } = {}) => [
|
|
866
|
+
{ event: "RUN_START", ts: ts(i), repo, issue: 100 + i }, ...extra, ...(code ? [{ event: "RUN_TERMINAL", reason, code }] : [])];
|
|
867
|
+
const deps = (eco, status, why = null) => ({ event: "DEPS_PREPARED", eco, status, reason: why });
|
|
868
|
+
const lanes = (n, f, o) => Array.from({ length: n }, (_, i) => lanesOfLog(lane(i, f(i), typeof o === "function" ? o(i) : o))[0]);
|
|
869
|
+
|
|
870
|
+
it("DEPS_PREPARED failed above 20% per ecosystem, with rate, sample and an example reason; healthy ecosystems stay quiet", () => {
|
|
871
|
+
const l = lanes(10, (i) => [deps("pnpm", i < 3 ? "failed" : "hit", "tool not found: spawn pnpm ENOENT"), deps("uv", "hit")]);
|
|
872
|
+
const a = computeAlerts(l, { repo: "o/r" });
|
|
873
|
+
assert.deepEqual(a.map((x) => [x.event, x.count, x.sample]), [["DEPS_PREPARED failed (pnpm)", 3, 10]]);
|
|
874
|
+
assert.equal(a[0].example, "tool not found: spawn pnpm ENOENT");
|
|
875
|
+
assert.deepEqual(computeAlerts(lanes(10, (i) => [deps("uv", i < 2 ? "failed" : "hit")]), { repo: "o/r" }), [], "exactly 20% is not above");
|
|
876
|
+
const line = renderAlerts(a)[0];
|
|
877
|
+
assert.match(line, /^DEPS_PREPARED failed \(pnpm\): 30% \(3\/10 lanes, threshold 20%\), e\.g\. "tool not found: spawn pnpm ENOENT"$/);
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it("deferred-to-ci above 30% of lanes and a TOOLCHAIN_WARNING repeated on more than 50% of lanes", () => {
|
|
881
|
+
const pre = (deferred) => ({ event: "PRECHECK_COMMANDS", round: 0, results: [{ command: "npm test", rc: 0, ms: 5 }, ...(deferred ? [{ command: "npx tsc", status: "deferred-to-ci" }] : [])] });
|
|
882
|
+
const warn = { event: "TOOLCHAIN_WARNING", warning: "host node v26 does not satisfy the .nvmrc pin \"22\"" };
|
|
883
|
+
const a = computeAlerts(lanes(10, (i) => [pre(i < 4), ...(i < 6 ? [warn] : [])]), { repo: "o/r" });
|
|
884
|
+
assert.deepEqual(a.map((x) => [x.event, x.count, x.sample, x.example]), [["deferred-to-ci", 4, 10, "npx tsc"], ["TOOLCHAIN_WARNING", 6, 10, warn.warning]]);
|
|
885
|
+
assert.deepEqual(computeAlerts(lanes(10, (i) => [pre(i < 3), ...(i < 5 ? [warn] : [])]), { repo: "o/r" }), [], "30% and 50% are not above");
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it("an outcome code whose share doubled against the previous window; needs a full previous window; completed never alerts", () => {
|
|
889
|
+
// 20 lanes, window 10: previous window has 1 ci.red (10%), current has 3 (30%)
|
|
890
|
+
const code = (i) => ({ code: [0, 12, 15, 18].includes(i) ? "ci.red" : "completed", reason: [0, 12, 15, 18].includes(i) ? "needs-human" : "merged" });
|
|
891
|
+
const l = lanes(20, () => [], code);
|
|
892
|
+
const a = computeAlerts(l, { repo: "o/r", window: 10 });
|
|
893
|
+
assert.deepEqual(a.map((x) => [x.event, x.count, x.sample, x.prevRate, x.example]), [["outcome ci.red", 3, 10, 0.1, "#118 needs-human"]]);
|
|
894
|
+
assert.match(renderAlerts(a)[0], /^outcome ci\.red: 30% \(3\/10 lanes, previous window 10%, threshold 2x\), e\.g\. "#118 needs-human"$/);
|
|
895
|
+
assert.deepEqual(computeAlerts(l.slice(5), { repo: "o/r", window: 10 }), [], "a previous window under min_lanes never compares");
|
|
896
|
+
assert.deepEqual(computeAlerts(lanes(20, () => [], (i) => ({ code: i < 15 ? "invalid" : "completed" })), { repo: "o/r", window: 10 }), []);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it("rolling window per repo, ordered by start: old failures and other repos fall out; under 10 lanes or no logs gives nothing", () => {
|
|
900
|
+
const bad = lanes(10, () => [deps("pnpm", "failed", "x")]);
|
|
901
|
+
const good = lanes(50, () => [deps("pnpm", "hit")]).map((l, i) => ({ ...l, startTs: ts(100 + i) }));
|
|
902
|
+
assert.deepEqual(computeAlerts([...good, ...bad], { repo: "o/r" }), [], "the 50 newest lanes are healthy");
|
|
903
|
+
assert.equal(computeAlerts(bad, { repo: "o/r" }).length, 1);
|
|
904
|
+
assert.deepEqual(computeAlerts(bad, { repo: "x/y" }), []);
|
|
905
|
+
assert.deepEqual(computeAlerts(bad.slice(1), { repo: "o/r" }), [], "9 lanes is too few");
|
|
906
|
+
assert.deepEqual(computeAlerts([], { repo: "o/r" }), []);
|
|
907
|
+
assert.deepEqual(readLanes(join(td, "missing")), []);
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
it("readLanes splits every RUN_START slice of every log (local files only) and keeps the repo", () => {
|
|
911
|
+
const dir = join(td, "runs"); mkdirSync(dir);
|
|
912
|
+
const text = (evs) => evs.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
913
|
+
writeFileSync(join(dir, "7.jsonl"), text([...lane(0, [deps("cargo", "failed", "lock drift")]), ...lane(1, [{ event: "WORKTREE_SPARSE_FALLBACK", reason: "no cone" }], { code: null })]) + "{torn");
|
|
914
|
+
writeFileSync(join(dir, "8.jsonl"), text(lane(2, [], { repo: "a/b" })));
|
|
915
|
+
const got = readLanes(dir);
|
|
916
|
+
assert.deepEqual(got.map((l) => [l.repo, l.issue, l.code, l.deps.length, l.sparseFallback]),
|
|
917
|
+
[["o/r", 100, "completed", 1, null], ["o/r", 101, null, 0, "no cone"], ["a/b", 102, "completed", 0, null]]);
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
it("thresholds come from forge.yaml stats.alerts.* (nested block and flat form) and change the result", () => {
|
|
921
|
+
assert.deepEqual(readAlertConfig(join(td, "none")), DEFAULT_ALERTS);
|
|
922
|
+
writeFileSync(join(td, "forge.yaml"), "stats:\n alerts:\n min_lanes: 5\n deps_failed_pct: 50\n alerts_window: 8\n window: 30\n deferred_to_ci_pct: nope\n");
|
|
923
|
+
const c = readAlertConfig(td);
|
|
924
|
+
assert.deepEqual(c, { ...DEFAULT_ALERTS, minLanes: 5, depsFailedPct: 50, window: 8 });
|
|
925
|
+
const l = lanes(6, (i) => [deps("pnpm", i < 2 ? "failed" : "hit", "x")]);
|
|
926
|
+
assert.deepEqual(computeAlerts(l, { repo: "o/r" }), [], "default min_lanes 10");
|
|
927
|
+
assert.deepEqual(computeAlerts(l, { repo: "o/r", ...c }), [], "33% is under the configured 50%");
|
|
928
|
+
assert.equal(computeAlerts(l, { repo: "o/r", ...c, depsFailedPct: 20 }).length, 1);
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it("stats report carries alerts and renders an ALERTS section only when one fired; --no-local skips them; no gh call for alerts", async () => {
|
|
932
|
+
const { gh } = fakeGh();
|
|
933
|
+
const localLanes = lanes(10, () => [deps("pnpm", "failed", "spawn pnpm ENOENT")]);
|
|
934
|
+
const rep = await buildReport({ repo: "o/r", local: true, cache: false, minN: 10 }, { gh, env: { ...ENV, FORGE_RUNS_DIR: join(td, "none") }, releaseTimes: [], localRuns: [], localLanes, alertConfig: {} });
|
|
935
|
+
assert.equal(rep.alerts.length, 1);
|
|
936
|
+
assert.match(renderMarkdown(rep), /## ALERTS\n\n- DEPS_PREPARED failed \(pnpm\): 100% \(10\/10 lanes/);
|
|
937
|
+
assert.equal(JSON.parse(renderJson(rep)).alerts[0].event, "DEPS_PREPARED failed (pnpm)");
|
|
938
|
+
const off = await buildReport({ repo: "o/r", local: false, cache: false, minN: 10 }, { gh, env: ENV, releaseTimes: [], localLanes });
|
|
939
|
+
assert.deepEqual(off.alerts, []);
|
|
940
|
+
assert.doesNotMatch(renderMarkdown(off), /ALERTS/);
|
|
941
|
+
// computed from local run logs alone: no gh in scope at all
|
|
942
|
+
assert.equal(computeAlerts(localLanes, { repo: "o/r" }).length, 1);
|
|
943
|
+
});
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
describe("env.tool_missing per release (forge#4414)", () => {
|
|
947
|
+
// The #4393 replay criterion is read as outcome-code counts per engine version; pin that the report surfaces them.
|
|
948
|
+
const run = (issue, version, reason, code) => ({ issue, repo: "o/r", version, reason, code, finished: true, usageEvents: 1, endTs: "2026-10-23T10:00:00Z", phases: { implement: { cr: 1, out: 1, ms: 1 } } });
|
|
949
|
+
const runs = [run(1, "1.30.0", "needs-human", "env.tool_missing"), run(2, "1.30.0", "merged", "completed"),
|
|
950
|
+
run(3, "1.31.0", "merged", "completed"), run(4, "1.31.0", "needs-human", "dependency.open")];
|
|
951
|
+
const codesOf = (g) => Object.fromEntries(g.flow.codes.map((c) => [c.code, c.count]));
|
|
952
|
+
|
|
953
|
+
it("counts env.tool_missing per version and drops pre-release lanes with sinceVersion", () => {
|
|
954
|
+
const all = aggregate({ runs }, { releaseTimes: [] });
|
|
955
|
+
assert.equal(codesOf(all.groups.find((g) => g.version === "1.30.0"))["env.tool_missing"], 1);
|
|
956
|
+
const after = aggregate({ runs }, { releaseTimes: [], sinceVersion: "1.31.0" });
|
|
957
|
+
assert.deepEqual(after.groups.map((g) => g.version), ["1.31.0"]);
|
|
958
|
+
assert.equal(codesOf(after.groups[0])["env.tool_missing"], undefined);
|
|
959
|
+
assert.equal(after.groups[0].flow.codesN, 2);
|
|
960
|
+
});
|
|
961
|
+
});
|
package/commands/orchestrate.md
CHANGED
|
@@ -100,7 +100,9 @@ remaining maintenance once no feature is pending; otherwise the rest ends `held
|
|
|
100
100
|
(live lanes are re-adopted, nothing is launched twice). The runner is hosted by the detached `forgedock schedd` daemon,
|
|
101
101
|
which resumes every daemon-hosted batch that has no report yet when it starts, so a closed session needs no manual
|
|
102
102
|
`--resume`; a batch started without the daemon still needs `forgedock orchestrate --resume <id>`. Change K of a running batch with
|
|
103
|
-
`forgedock orchestrate --set-parallel <id> K`.
|
|
103
|
+
`forgedock orchestrate --set-parallel <id> K`. Issues filed while a batch runs join it with
|
|
104
|
+
`forgedock orchestrate --add <id> N...` (or `--resume <id> N...`), under its K and DAG; never start a second batch
|
|
105
|
+
for them. Its pre-filter still closes already-fixed ones. The runner requeues `deferred` lanes by code (host busy after its
|
|
104
106
|
retry-in, a usage limit holds admissions for that account (backend identity) until the reset, across every batch on
|
|
105
107
|
the host; `forgedock limits show|clear <id>|--all` lists or drops holds; transient merge/push/idle failures back off; non-host
|
|
106
108
|
deferrals stop after `orchestration.max_deferrals`), relaunches an `engine-error` once, and stops admitting after
|
|
@@ -210,7 +210,7 @@ Append this block at the very end of your comment (after the `---` footer line,
|
|
|
210
210
|
8. **HTML comments**: The block is invisible in rendered markdown but parseable by the review system
|
|
211
211
|
9. **Agent marker**: Include exactly one `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker in the persisted body, where `{domain}` is the marker domain the orchestrator passes you (the canonical value from the roster-to-marker table in `review-pr.md`, e.g. `infra` and `scraper`; never a lowercased display name such as `infrastructure` or `scraping`).
|
|
212
212
|
10. **Reviewed SHA**: Include exactly one line `Reviewed-SHA: [REVIEW_SHA]` (the full 40-hex head you were pinned to) in the persisted body, outside the marker. `/review-pr` scopes its completion wait and idempotent re-entry to this line, so a comment without the current head does not count as a completed review. Do NOT put the SHA inside the `<!-- FORGE:REVIEW-AGENT:{domain} -->` marker.
|
|
213
|
-
11. **Failure scenario and scope (provenance inputs)**: every finding's body section (the prose above the findings block) MUST carry a `**Failure scenario**` line — concrete inputs leading to a wrong output or behaviour — and a `**Scope**: changed` or `**Scope**: pre-existing` line (`changed` = the cited line is added or modified by this PR; `pre-existing` = it was already there). `/review-pr` §6B.4 reads these: a finding without a failure scenario is demoted to a note (unless CRITICAL/HIGH, any confidence), and a MEDIUM or lower finding whose cited line is not in the PR diff is routed as pre-existing (CRITICAL/HIGH never are; see §5). The `**Scope**` tag is informational and never gates routing. Speculative hardening with no failure scenario is a note, not a finding. The one-line `<!-- FINDING:... -->` grammar above is unchanged. <!-- Added: forge#3452 -->
|
|
213
|
+
11. **Failure scenario and scope (provenance inputs)**: every finding's body section (the prose above the findings block) MUST carry a `**Failure scenario**` line — concrete inputs leading to a wrong output or behaviour — and a `**Scope**: changed` or `**Scope**: pre-existing` line (`changed` = the cited line is added or modified by this PR; `pre-existing` = it was already there). `/review-pr` §6B.4 reads these: a finding without a failure scenario is demoted to a note (unless CRITICAL/HIGH, any confidence, or CONFIRMED MEDIUM; a plain `Failure scenario:`, `Scenario:`, `Repro:` or `Steps to reproduce:` line also counts as the scenario), and a MEDIUM or lower finding whose cited line is not in the PR diff is routed as pre-existing (CRITICAL/HIGH never are; see §5). The `**Scope**` tag is informational and never gates routing. Speculative hardening with no failure scenario is a note, not a finding. The one-line `<!-- FINDING:... -->` grammar above is unchanged. <!-- Added: forge#3452 -->
|
|
214
214
|
12. **Fix efficacy (linked-issue contradiction)**: when a finding says the PR's linked issue is NOT fixed, or that the PR re-introduces the defect it claims to fix, its body section MUST carry a `**Fix efficacy**: fails` (the fix does not work) or `**Fix efficacy**: regresses` (the PR re-introduces the defect) line, and the prose may include the token on its own line (`FIX_EFFICACY: fails|regresses`; line-anchored, a mid-sentence mention does not count). Opt-in on the tag, never on keywords. `/review-pr` §6B.5 passes it to `scripts/classify-finding.sh --fix-efficacy`, which exempts a MEDIUM finding from the fix-PR damper. Use it only for the linked issue's own fix; never for unrelated defects. The one-line `<!-- FINDING:... -->` grammar is unchanged. <!-- Added: forge#3693 -->
|
|
215
215
|
|
|
216
216
|
### Domain Prefixes
|