forgedock 1.29.1 → 1.31.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 +5 -2
- package/bin/engine/phases.mjs +20 -3
- 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 +453 -66
- package/bin/engine.mjs +50 -38
- package/bin/forgedock.mjs +35 -3
- package/bin/gc.mjs +50 -17
- 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 +356 -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 +278 -22
- package/bin/lean/conformance.test.mjs +251 -5
- package/bin/lean/context.mjs +133 -55
- package/bin/lean/context.test.mjs +175 -9
- 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 +55 -19
- package/bin/lean/enumerating.mjs +189 -0
- package/bin/lean/enumerating.test.mjs +97 -0
- 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 +799 -127
- package/bin/lean/lean.test.mjs +1749 -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/limits.test.mjs +36 -1
- package/bin/lean/orchestrate.mjs +341 -99
- 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 +180 -0
- package/bin/lean/precheck.test.mjs +205 -0
- package/bin/lean/prefilter.mjs +180 -9
- package/bin/lean/prefilter.test.mjs +175 -3
- package/bin/lean/preflight.mjs +238 -0
- package/bin/lean/preflight.test.mjs +108 -0
- package/bin/lean/probe.mjs +297 -0
- package/bin/lean/probe.test.mjs +210 -0
- package/bin/lean/prompts.mjs +134 -12
- package/bin/lean/recovery.test.mjs +75 -0
- 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.test.mjs +127 -1
- package/bin/lean/schedd.mjs +55 -9
- package/bin/lean/schedd.test.mjs +149 -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 +385 -0
- package/bin/lean/sequence.test.mjs +373 -0
- package/bin/lean/testmap.mjs +43 -6
- package/bin/lean/testmap.test.mjs +38 -1
- 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 +77 -10
- package/bin/lean/verify.test.mjs +74 -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 +319 -0
- package/bin/stats/efficiency.test.mjs +233 -0
- package/bin/stats/github-source.mjs +8 -3
- package/bin/stats/index.mjs +69 -6
- package/bin/stats/local-source.mjs +86 -3
- package/bin/stats/query.mjs +48 -0
- package/bin/stats/render.mjs +22 -0
- package/bin/stats/stats.test.mjs +224 -5
- package/commands/orchestrate.md +6 -2
- 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 +88 -6
- package/docs/OUTCOME-CODES.md +7 -4
- package/docs/RELEASE-NOTES.md +10 -0
- package/docs/STATS.md +53 -1
- package/docs/TELEMETRY.md +1 -1
- package/docs/spec/review-protocol.md +1 -1
- package/package.json +1 -1
- package/scripts/acceptance-red-green.sh +13 -4
- package/scripts/acceptance-red-green.test.sh +14 -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/test-map.sh +63 -17
- package/scripts/test-map.test.sh +55 -0
- package/scripts/trusted-comments.test.sh +1 -1
package/bin/lean/checks.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import fs from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
|
-
import { git, changedFiles, sha256, homeForgeDir, shellSplit, shellQuote, globToRegex } from "../../scripts/ci-local/lib.mjs";
|
|
16
|
+
import { git, changedFiles, sha256, homeForgeDir, shellSplit, shellQuote, globToRegex, parseYaml, trustedForgeYaml } from "../../scripts/ci-local/lib.mjs";
|
|
17
17
|
import { makeScopeCtx, scopeCommand } from "../../scripts/ci-local/scoping.mjs";
|
|
18
18
|
|
|
19
19
|
const px = path.posix;
|
|
@@ -67,16 +67,16 @@ export function discoverChecks(repo) {
|
|
|
67
67
|
const turbo = turboTasks(repo);
|
|
68
68
|
const scripts = new Map(pkgDirs.map((d) => [d, (readJson(path.join(repo, d, "package.json")) || {}).scripts || {}]));
|
|
69
69
|
for (const [script, kind] of Object.entries(SCRIPT_KINDS)) {
|
|
70
|
-
if (turbo.has(script)) { out.push({ kind, command: `${exists(repo, "node_modules/.bin/turbo") ? "" : "npx "}turbo run ${script}`, cwd: "." }); continue; }
|
|
70
|
+
if (turbo.has(script)) { out.push({ kind, command: `${exists(repo, "node_modules/.bin/turbo") ? "" : "npx "}turbo run ${script}`, cwd: ".", eco: "js" }); continue; }
|
|
71
71
|
const ws = pkgDirs.filter((d) => d !== "." && typeof scripts.get(d)[script] === "string").sort();
|
|
72
72
|
const dirs = ws.length ? ws : typeof (scripts.get(".") || {})[script] === "string" ? ["."] : [];
|
|
73
|
-
for (const d of dirs) out.push({ kind, command: `${pm} run ${script}`, cwd: d, exec: EXEC[pm] });
|
|
73
|
+
for (const d of dirs) out.push({ kind, command: `${pm} run ${script}`, cwd: d, exec: EXEC[pm], eco: "js" });
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
const pyproject = readText(path.join(repo, "pyproject.toml"));
|
|
77
77
|
const run = exists(repo, "uv.lock") ? "uv run " : exists(repo, "poetry.lock") ? "poetry run " : "";
|
|
78
|
-
if (/^\[tool\.ruff/m.test(pyproject) || exists(repo, "ruff.toml") || exists(repo, ".ruff.toml")) out.push({ kind: "lint", command: `${run}ruff check .`, cwd: "." });
|
|
79
|
-
if (/^\[tool\.pytest/m.test(pyproject) || exists(repo, "pytest.ini") || tracked.some((f) => px.basename(f) === "conftest.py")) out.push({ kind: "test", command: `${run}pytest`, cwd: "." });
|
|
78
|
+
if (/^\[tool\.ruff/m.test(pyproject) || exists(repo, "ruff.toml") || exists(repo, ".ruff.toml")) out.push({ kind: "lint", command: `${run}ruff check .`, cwd: ".", eco: "py" });
|
|
79
|
+
if (/^\[tool\.pytest/m.test(pyproject) || exists(repo, "pytest.ini") || tracked.some((f) => px.basename(f) === "conftest.py")) out.push({ kind: "test", command: `${run}pytest`, cwd: ".", eco: "py" });
|
|
80
80
|
return out;
|
|
81
81
|
}
|
|
82
82
|
|
|
@@ -150,16 +150,39 @@ export function laneChanges(repo, base) {
|
|
|
150
150
|
return { files: files.filter((f) => !f.startsWith(".forge-lean/")), from };
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
/**
|
|
154
|
-
|
|
153
|
+
/**
|
|
154
|
+
* The repo's configured `verification.commands.typescript.lint` (#4381), read only from the trusted forge.yaml copy
|
|
155
|
+
* (forge#4040: never the PR's own tree). null when absent, empty or unreadable (no yq, parse error).
|
|
156
|
+
*/
|
|
157
|
+
export function configuredLint(repo, base) {
|
|
158
|
+
try {
|
|
159
|
+
const mb = git(repo, ["merge-base", base, "HEAD"]).out.trim();
|
|
160
|
+
const { text } = trustedForgeYaml(repo, base, mb);
|
|
161
|
+
if (!text) return null;
|
|
162
|
+
const v = parseYaml(text)?.verification?.commands?.typescript?.lint;
|
|
163
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
164
|
+
} catch { return null; }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* The scoped checks for this lane: [{kind, command, cwd, mode, widen_reason, selector, reason, configured}]. A configured
|
|
169
|
+
* typescript.lint replaces the discovered JS lint checks (#4381); Python checks are never replaced.
|
|
170
|
+
*/
|
|
171
|
+
export function scopedChecks({ repo, base, checks = discoverChecks(repo), lint = configuredLint(repo, base) }) {
|
|
155
172
|
const { files, from } = laneChanges(repo, base);
|
|
156
173
|
const sctx = makeScopeCtx({ repo, baseRef: base, from, changed: files });
|
|
174
|
+
if (lint) {
|
|
175
|
+
const i = checks.findIndex((c) => c.kind === "lint" && c.eco === "js");
|
|
176
|
+
const own = { kind: "lint", command: lint, cwd: ".", configured: true, ...(exists(repo, "package.json") ? { exec: EXEC[packageManager(repo)] } : {}) };
|
|
177
|
+
checks = i < 0 ? [...checks, own] : [...checks.slice(0, i), own, ...checks.slice(i).filter((c) => !(c.kind === "lint" && c.eco === "js"))];
|
|
178
|
+
}
|
|
157
179
|
return checks.map((c) => {
|
|
158
180
|
const r = scopeCommand(c.command, c.cwd === "." ? "" : c.cwd, sctx);
|
|
159
181
|
// A resolved script body (`pnpm run typecheck` -> `tsc -p apps/web`) runs its binary through the package manager.
|
|
160
182
|
const resolved = r.action !== "skip" && c.exec && r.command !== c.command && !SYSTEM_BINS.has(px.basename((shellSplit(r.command) || [""])[0]));
|
|
161
183
|
const out = { kind: c.kind, cwd: c.cwd, command: r.action === "skip" ? "" : resolved ? `${c.exec} ${r.command}` : r.command, mode: r.mode, widen_reason: r.widen_reason || "", selector: r.selector || "" };
|
|
162
184
|
if (r.reason) out.reason = r.reason;
|
|
185
|
+
if (c.configured) out.configured = true;
|
|
163
186
|
return r.action === "skip" ? out : withCacheFlags(out, repo);
|
|
164
187
|
});
|
|
165
188
|
}
|
|
@@ -190,8 +213,8 @@ function main(argv) {
|
|
|
190
213
|
if (!checks.length) console.log("# no typecheck/lint/test command found");
|
|
191
214
|
for (const c of checks) {
|
|
192
215
|
const where = c.cwd && c.cwd !== "." ? `(cd ${shellQuote(c.cwd)} && ${c.command})` : c.command;
|
|
193
|
-
if (c.mode === "skip") console.log(`${c.kind} skip # skipped: ${c.reason || "not affected"}`);
|
|
194
|
-
else console.log(`${c.kind} ${c.mode} ${where}${c.widen_reason ? ` # widened: ${c.widen_reason}` : ""}`);
|
|
216
|
+
if (c.mode === "skip") console.log(`${c.kind} skip # skipped: ${c.reason || "not affected"}${c.configured ? " (configured: verification.commands.typescript.lint)" : ""}`);
|
|
217
|
+
else console.log(`${c.kind} ${c.mode} ${where}${c.widen_reason ? ` # widened: ${c.widen_reason}` : ""}${c.configured ? " # configured: verification.commands.typescript.lint" : ""}`);
|
|
195
218
|
}
|
|
196
219
|
return 0;
|
|
197
220
|
}
|
package/bin/lean/checks.test.mjs
CHANGED
|
@@ -7,7 +7,8 @@ import fs from "node:fs";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { execFileSync } from "node:child_process";
|
|
9
9
|
import { tmpDir } from "../tests/_tmp.mjs";
|
|
10
|
-
import { discoverChecks, scopedChecks, cacheEntry, hydrate, publish, withCacheFlags, LOCAL_CACHE } from "./checks.mjs";
|
|
10
|
+
import { discoverChecks, scopedChecks, configuredLint, cacheEntry, hydrate, publish, withCacheFlags, LOCAL_CACHE } from "./checks.mjs";
|
|
11
|
+
import { yqAvailable } from "../../scripts/ci-local/lib.mjs";
|
|
11
12
|
|
|
12
13
|
const TMP = tmpDir("checks-4218-");
|
|
13
14
|
process.env.FORGE_CI_LOCAL_HOME = path.join(TMP, "forge-home");
|
|
@@ -181,3 +182,79 @@ test("discovery: declared workspace packages' scripts win over the root; undecla
|
|
|
181
182
|
});
|
|
182
183
|
assert.deepEqual(discoverChecks(r).map((c) => `${c.kind}:${c.command}:${c.cwd}`), ["typecheck:pnpm run typecheck:apps/web", "test:pnpm run test:."]);
|
|
183
184
|
});
|
|
185
|
+
|
|
186
|
+
// #4381: lint is scoped to the changed packages, never their dependency closure; a configured typescript.lint wins.
|
|
187
|
+
const HAS_YQ = yqAvailable();
|
|
188
|
+
const turboMonorepo = (extra = {}) => repo({
|
|
189
|
+
".gitignore": "node_modules/\nforge.yaml\n",
|
|
190
|
+
"package.json": JSON.stringify({ name: "root", private: true, workspaces: ["apps/*", "packages/*"] }),
|
|
191
|
+
"pnpm-lock.yaml": "lockfileVersion: '9.0'\n",
|
|
192
|
+
"turbo.json": JSON.stringify({ tasks: { lint: {}, typecheck: {}, test: {} } }),
|
|
193
|
+
"packages/db/package.json": JSON.stringify({ name: "db", scripts: { lint: "eslint ." } }),
|
|
194
|
+
"packages/db/src/index.ts": "export const db = 1;\n",
|
|
195
|
+
"apps/api/package.json": JSON.stringify({ name: "api", dependencies: { db: "workspace:*" } }),
|
|
196
|
+
"apps/api/src/index.ts": "export const api = 1;\n",
|
|
197
|
+
...extra,
|
|
198
|
+
});
|
|
199
|
+
const TS_LINT_YAML = "verification:\n commands:\n typescript:\n lint: \"pnpm exec eslint --cache packages/db/src\"\n";
|
|
200
|
+
|
|
201
|
+
test("turbo monorepo: an upstream package change lints only the changed packages; typecheck/test keep dependents", () => {
|
|
202
|
+
const r = turboMonorepo();
|
|
203
|
+
touch(r, "packages/db/src/index.ts");
|
|
204
|
+
const c = byKind(scopedChecks({ repo: r, base: "main", lint: null }));
|
|
205
|
+
assert.equal(c.lint.selector, "turbo-changed");
|
|
206
|
+
assert.match(c.lint.command, /^npx turbo run lint '--filter=\[[0-9a-f]{40}\]'$/);
|
|
207
|
+
assert.doesNotMatch(c.lint.command, /\.\.\.\[/);
|
|
208
|
+
assert.match(c.typecheck.command, /^npx turbo run typecheck '--filter=\.\.\.\[[0-9a-f]{40}\]'$/);
|
|
209
|
+
assert.match(c.test.command, /^npx turbo run test '--filter=\.\.\.\[[0-9a-f]{40}\]'$/);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("turbo: lint-only task lists (lint:*, *:lint) scope to changed packages; mixed lists keep the closure", () => {
|
|
213
|
+
const r = turboMonorepo();
|
|
214
|
+
touch(r, "packages/db/src/index.ts");
|
|
215
|
+
const run = (command) => scopedChecks({ repo: r, base: "main", checks: [{ kind: "lint", command, cwd: "." }], lint: null })[0];
|
|
216
|
+
assert.equal(run("turbo run lint:types web:lint --concurrency 2").selector, "turbo-changed");
|
|
217
|
+
assert.equal(run("turbo run lint build").selector, "turbo-affected");
|
|
218
|
+
assert.equal(run("turbo run lint --filter=web").mode, "full");
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("configured typescript.lint in the main checkout's forge.yaml replaces the discovered lint", { skip: !HAS_YQ && "yq not installed" }, () => {
|
|
222
|
+
const r = turboMonorepo();
|
|
223
|
+
fs.writeFileSync(path.join(r, "forge.yaml"), TS_LINT_YAML);
|
|
224
|
+
const wt = path.join(TMP, `wt${n++}`);
|
|
225
|
+
git(r, "worktree", "add", "-q", "-b", "lane", wt, "main");
|
|
226
|
+
touch(wt, "packages/db/src/index.ts");
|
|
227
|
+
const checks = scopedChecks({ repo: wt, base: "main" });
|
|
228
|
+
const lints = checks.filter((c) => c.kind === "lint");
|
|
229
|
+
assert.equal(lints.length, 1);
|
|
230
|
+
assert.equal(lints[0].configured, true);
|
|
231
|
+
assert.doesNotMatch(lints[0].command, /turbo/);
|
|
232
|
+
assert.match(lints[0].command, /eslint .*packages\/db\/src/);
|
|
233
|
+
assert.deepEqual(checks.map((c) => c.kind), ["typecheck", "lint", "test"]);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("a PR-tracked forge.yaml is not trusted; without the slot the output equals discovery-only", { skip: !HAS_YQ && "yq not installed" }, () => {
|
|
237
|
+
const r = turboMonorepo();
|
|
238
|
+
touch(r, "packages/db/src/index.ts");
|
|
239
|
+
const before = scopedChecks({ repo: r, base: "main", lint: null });
|
|
240
|
+
assert.deepEqual(scopedChecks({ repo: r, base: "main" }), before);
|
|
241
|
+
fs.writeFileSync(path.join(r, "forge.yaml"), "verification:\n commands:\n python:\n lint: \"ruff check .\"\n");
|
|
242
|
+
assert.deepEqual(scopedChecks({ repo: r, base: "main" }), before);
|
|
243
|
+
fs.writeFileSync(path.join(r, "forge.yaml"), TS_LINT_YAML);
|
|
244
|
+
git(r, "add", "-f", "forge.yaml");
|
|
245
|
+
git(r, "commit", "-qm", "pr config");
|
|
246
|
+
assert.equal(configuredLint(r, "main"), null);
|
|
247
|
+
const lint = scopedChecks({ repo: r, base: "main" }).find((c) => c.kind === "lint");
|
|
248
|
+
assert.equal(lint.configured, undefined);
|
|
249
|
+
assert.match(lint.command, /^npx turbo run lint/);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("Python: a configured typescript.lint or python.lint never replaces ruff", { skip: !HAS_YQ && "yq not installed" }, () => {
|
|
253
|
+
const r = pyRepo("uv.lock", "version = 1\n");
|
|
254
|
+
fs.appendFileSync(path.join(r, ".gitignore"), "forge.yaml\n");
|
|
255
|
+
git(r, "commit", "-qam", "ignore forge.yaml");
|
|
256
|
+
fs.writeFileSync(path.join(r, "forge.yaml"), "verification:\n commands:\n python:\n lint: \"ruff check src\"\n");
|
|
257
|
+
fs.appendFileSync(path.join(r, "pkg/alpha.py"), "# change\n");
|
|
258
|
+
assert.equal(configuredLint(r, "main"), null);
|
|
259
|
+
assert.equal(byKind(scopedChecks({ repo: r, base: "main" })).lint.command, "uv run ruff check pkg/alpha.py");
|
|
260
|
+
});
|
|
@@ -14,6 +14,9 @@ import path from "node:path";
|
|
|
14
14
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
15
15
|
import { tmpDir } from "../tests/_tmp.mjs";
|
|
16
16
|
import { runCore } from "./lean.mjs";
|
|
17
|
+
import { readLog } from "../engine/runlog.mjs";
|
|
18
|
+
import { runCheckCommand } from "./effects.mjs";
|
|
19
|
+
import { checkFromCli } from "../engine-cli.mjs";
|
|
17
20
|
|
|
18
21
|
const GIT_ENV = { ...process.env, GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@example.invalid", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@example.invalid", GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_NOSYSTEM: "1" };
|
|
19
22
|
const git = (args, cwd) => { try { return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], env: GIT_ENV }); } catch (e) { throw Object.assign(new Error(`git ${args.join(" ")}: ${e.stderr || e.message}`), { stderr: String(e.stderr ?? "") }); } };
|
|
@@ -134,7 +137,7 @@ function builderScript(cs) {
|
|
|
134
137
|
const finish = (t) => { res.validation = [{ id: "V1", command: V1, exit: t.run(V1) }]; t.lean("implementation.md", `### Approach\nrelease in a catch\n\n### Changes\n${t.changed().map((f) => `- ${f}`).join("\n")}\n\n### Acceptance Criteria Status\n- see result\n\n### Verification\n- ${V1}\n\n### Notes\nnone`); t.lean("result.json", res); };
|
|
135
138
|
return async (n, c, t) => {
|
|
136
139
|
if (n === 1) { t.lean("investigation.md", "**Verdict**: CONFIRMED\n**Confidence**: high\n**Severity**: high\n**Task Type**: bug\n\n### Root Cause\ncheck() throws after reserve() with no release"); t.lean("contract.json", cs.contract()); return; }
|
|
137
|
-
if (n === 2) { cs.impl(t); t.commit("fix: release on 429"); res = { status: "fixed", summary: "release on 429", requirements: (cs.claims ?? claimAll)(cs.contract()), amendments: [] }; finish(t); return; }
|
|
140
|
+
if (n === 2) { cs.impl(t); t.commit("fix: release on 429"); if (cs.selfCheck) await cs.selfCheck(c.cwd); res = { status: "fixed", summary: "release on 429", requirements: (cs.claims ?? claimAll)(cs.contract()), amendments: [] }; finish(t); return; }
|
|
138
141
|
if (cs.resume) { cs.resume(t, res); t.commit("fix: conformance"); }
|
|
139
142
|
finish(t);
|
|
140
143
|
};
|
|
@@ -162,11 +165,12 @@ function oracle(c) {
|
|
|
162
165
|
}
|
|
163
166
|
const generic = () => "```json\n" + JSON.stringify({ verdict: "PASS", fixes_issue: true, regression: false, reason: "looks good" }) + "\n```";
|
|
164
167
|
|
|
165
|
-
async function runCase(cs, conformance = "enforce") {
|
|
168
|
+
async function runCase(cs, conformance = "enforce", extra = {}) {
|
|
166
169
|
const dir = tmpDir("conf-e2e-");
|
|
167
170
|
const w = makeWorld(dir, { builder: builderScript(cs), reviewer: (n, c) => (cs.generic ? generic() : oracle(c)) });
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
if (extra.runCommand) w.io.runCommand = extra.runCommand;
|
|
172
|
+
const ret = await runCore({ issue: 101, repo: "o/r", lane: "staging", cwd: w.repo, worktreeRoot: path.join(dir, "wt"), dir: path.join(dir, "runs"), io: w.io, random: () => 0.5, stableMs: 0, livePollMs: 20, conformance, ...(extra.opts ?? {}) });
|
|
173
|
+
return { ...w.observe(ret), log: readLog(path.join(dir, "runs"), 101, { repo: "o/r" }) }; // #4432: keyed by repo
|
|
170
174
|
}
|
|
171
175
|
const blocked = (o, code) => {
|
|
172
176
|
assert.equal(o.ret.terminalReason, "needs-human"); assert.equal(o.ret.code, code);
|
|
@@ -185,7 +189,7 @@ describe("conformance gate end-to-end (real git, disposable repo)", { skip: !HAV
|
|
|
185
189
|
const o = await runCase({ contract: FULL, impl: (t) => fix(t, ["scrape", "search", "export"]) });
|
|
186
190
|
merged(o); assert.ok(o.staging.search && o.staging.export && !o.staging.billingChanged);
|
|
187
191
|
assert.deepEqual([o.counts.builder, o.counts.reviewer], [2, 1]);
|
|
188
|
-
assert.deepEqual(o.markers, ["FORGE:STARTED", "FORGE:INVESTIGATION", "FORGE:CONTRACT", "FORGE:IMPLEMENTATION", "FORGE:REVIEW", "FORGE:TRAJECTORY"]);
|
|
192
|
+
assert.deepEqual(o.markers, ["FORGE:STARTED", "FORGE:INVESTIGATION", "FORGE:CONTRACT", "FORGE:IMPLEMENTATION", "FORGE:REVIEW", "FORGE:FACTS", "FORGE:TRAJECTORY"]);
|
|
189
193
|
});
|
|
190
194
|
it("2 omission: the contract silently drops requirement 3; review catches it; issue stays open, nothing merged", async () => {
|
|
191
195
|
const o = await runCase({ contract: OMIT, impl: (t) => fix(t, ["scrape", "search"]) });
|
|
@@ -225,6 +229,24 @@ describe("conformance gate end-to-end (real git, disposable repo)", { skip: !HAV
|
|
|
225
229
|
assert.match(o.issue.comments.find((c) => c.includes("FORGE:REVIEW")), /would BLOCK \(nonconforming\)[\s\S]*conformance\.omission/);
|
|
226
230
|
assert.deepEqual([o.counts.builder, o.counts.reviewer], [2, 1]);
|
|
227
231
|
});
|
|
232
|
+
it("#4364 forgedock check in the lane worktree runs the engine's commands and reaches its verdict", async () => {
|
|
233
|
+
let cli = null;
|
|
234
|
+
const learned = [V1, "exit 3"];
|
|
235
|
+
const o = await runCase({ contract: FULL, impl: (t) => fix(t, ["scrape", "search", "export"]),
|
|
236
|
+
selfCheck: async (wt) => { cli = await checkFromCli([], { cwd: wt, git: async (a, cwd) => git(a, cwd), out: () => {} }); } },
|
|
237
|
+
"enforce", { runCommand: runCheckCommand, opts: { precheck: { commands: learned }, verify: { maxRounds: 0 } } });
|
|
238
|
+
merged(o);
|
|
239
|
+
const [engine] = o.log.filter((e) => e.event === "PRECHECK_COMMANDS");
|
|
240
|
+
assert.deepEqual(cli.commands.map(({ command, rc }) => ({ command, rc })), engine.results.map(({ command, rc }) => ({ command, rc })));
|
|
241
|
+
assert.deepEqual(engine.results.map((r) => r.rc), [0, 3], "one passing and one failing pre-PR command");
|
|
242
|
+
// validate: the CLI's V1 run agrees with the engine's conformance precheck (no validate failure)
|
|
243
|
+
assert.deepEqual(cli.validate.map(({ id, rc }) => ({ id, rc })), [{ id: "V1", rc: 0 }]);
|
|
244
|
+
assert.ok(!o.log.some((e) => e.event === "PRECHECK" && e.failures.length), "the engine's validate verdict passes too");
|
|
245
|
+
assert.equal(cli.ok, false); assert.equal(cli.exitCode, 1);
|
|
246
|
+
// the run was on the final tree, so the engine records it as red (not skipped/stale) on both events
|
|
247
|
+
assert.equal(engine.self_check, "red");
|
|
248
|
+
assert.deepEqual(o.log.filter((e) => e.event === "SELF_CHECK_MISSING").map((e) => e.state), ["red"]);
|
|
249
|
+
});
|
|
228
250
|
it("6 a generic PASS over an incomplete implementation never merges (review.incomplete)", async () => {
|
|
229
251
|
const o = await runCase({ contract: FULL, impl: (t) => fix(t, ["scrape", "export"]), generic: true });
|
|
230
252
|
blocked(o, "review.incomplete"); assert.deepEqual([o.counts.builder, o.counts.reviewer], [2, 2]);
|