diff --git a/packages/loopover-engine/src/review/guardrail-config.ts b/packages/loopover-engine/src/review/guardrail-config.ts index 1277463b6d..57dd22b721 100644 --- a/packages/loopover-engine/src/review/guardrail-config.ts +++ b/packages/loopover-engine/src/review/guardrail-config.ts @@ -51,6 +51,14 @@ export const ENGINE_DECISION_GUARDRAIL_GLOBS = [ "src/review/cutover-gate.ts", "src/review/linked-issue-hard-rules.ts", "src/review/outcomes-wire.ts", + // #8698: the guardrail-matching engine and the gate-decision core themselves were a self-referential blind + // spot -- none of the packages/loopover-engine files that actually decide a PR's disposition were listed, so + // a PR weakening the glob matcher, flipping a block/advisory disposition, or rewiring the orchestrator would + // not itself trip the hard guardrail. Listed on the engine side (no src/ shim exists for these) for the same + // reason the autonomy/guardrail-config real paths are: it is the substantive logic a PR edit would change. + "packages/loopover-engine/src/signals/change-guardrail.ts", + "packages/loopover-engine/src/advisory/gate-advisory.ts", + "packages/loopover-engine/src/predicted-gate.ts", ]; // Default, safe-by-default invariant set (restored by #3943 after the original pure-config-as-code design diff --git a/test/unit/guardrail-config.test.ts b/test/unit/guardrail-config.test.ts index febe8a3b6c..d5a838c47a 100644 --- a/test/unit/guardrail-config.test.ts +++ b/test/unit/guardrail-config.test.ts @@ -37,6 +37,31 @@ describe("ENGINE_DECISION_GUARDRAIL_GLOBS — post-#6203-migration real paths (# }); }); +// #8698: the guardrail-matching engine itself (change-guardrail.ts), the gate-decision advisory core +// (gate-advisory.ts), and the predicted-gate orchestrator were never listed -- a self-referential blind spot, +// since a PR weakening exactly those files would not itself trip the hard guardrail. Each must now be a hit. +describe("ENGINE_DECISION_GUARDRAIL_GLOBS — guardrail engine + gate-advisory/predicted-gate self-protection (#8698)", () => { + it("lists the guardrail-matching engine, the gate-advisory core, and the predicted-gate orchestrator", () => { + expect(ENGINE_DECISION_GUARDRAIL_GLOBS).toContain("packages/loopover-engine/src/signals/change-guardrail.ts"); + expect(ENGINE_DECISION_GUARDRAIL_GLOBS).toContain("packages/loopover-engine/src/advisory/gate-advisory.ts"); + expect(ENGINE_DECISION_GUARDRAIL_GLOBS).toContain("packages/loopover-engine/src/predicted-gate.ts"); + }); + + it("a PR touching only the guardrail-matching engine trips the hard guardrail", () => { + expect(isGuardrailHit(["packages/loopover-engine/src/signals/change-guardrail.ts"], resolveHardGuardrailGlobs(null))).toBe( + true, + ); + }); + + it("a PR touching only the gate-advisory core trips the hard guardrail", () => { + expect(isGuardrailHit(["packages/loopover-engine/src/advisory/gate-advisory.ts"], resolveHardGuardrailGlobs(null))).toBe(true); + }); + + it("a PR touching only the predicted-gate orchestrator trips the hard guardrail", () => { + expect(isGuardrailHit(["packages/loopover-engine/src/predicted-gate.ts"], resolveHardGuardrailGlobs(null))).toBe(true); + }); +}); + describe("resolveHardGuardrailGlobs", () => { it("uses invariant guardrails when effective settings omit hardGuardrailGlobs", () => { expect(resolveHardGuardrailGlobs(undefined)).toEqual(DEFAULT_HARD_GUARDRAIL_GLOBS);