Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 50 additions & 3 deletions packages/gittensory-engine/src/review/guardrail-config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,59 @@
import type { RepositorySettings } from "../types/predicted-gate-types.js";

export const CONFIG_AS_CODE_GUARDRAIL_GLOBS = [
".gittensory.yml",
".gittensory.yaml",
".gittensory.json",
".github/gittensory.yml",
".github/gittensory.yaml",
".github/gittensory.json",
"**/codecov.yml",
"**/codecov.yaml",
"**/.codecov.yml",
];

export const WORKFLOW_AND_RUNTIME_GUARDRAIL_GLOBS = [
".github/workflows/**",
"scripts/**",
"wrangler.jsonc",
"src/selfhost/**",
];

export const ENGINE_DECISION_GUARDRAIL_GLOBS = [
"src/rules/**",
"src/services/**",
"src/settings/agent-actions.ts",
"src/settings/agent-execution.ts",
"src/settings/agent-sweep.ts",
"src/settings/autonomy.ts",
"src/queue/**",
"src/github/pr-actions.ts",
"src/github/app.ts",
"src/github/backfill.ts",
"src/scoring/**",
"src/auth/**",
"src/review/safety.ts",
"src/review/guardrail-config.ts",
"src/review/cutover-gate.ts",
"src/review/linked-issue-hard-rules.ts",
"src/review/outcomes-wire.ts",
];

export const DEFAULT_HARD_GUARDRAIL_GLOBS = [
...CONFIG_AS_CODE_GUARDRAIL_GLOBS,
...WORKFLOW_AND_RUNTIME_GUARDRAIL_GLOBS,
...ENGINE_DECISION_GUARDRAIL_GLOBS,
];

/**
* Resolve hard-guardrail path globs from the already-effective repo settings. Path holds are config-as-code only:
* omitted/null settings mean no path guardrails, and arrays replace lower layers wholesale.
* Resolve hard-guardrail path globs from the already-effective repo settings. Built-in config-as-code,
* workflow/runtime, and engine decision guardrails are invariants; repo settings may only add globs.
*/
export function resolveHardGuardrailGlobs(
settings: Pick<RepositorySettings, "hardGuardrailGlobs"> | null | undefined,
): string[] {
const configured = settings?.hardGuardrailGlobs;
return Array.isArray(configured) ? [...configured] : [];
return Array.from(
new Set([...DEFAULT_HARD_GUARDRAIL_GLOBS, ...(Array.isArray(configured) ? configured : [])]),
);
}
53 changes: 50 additions & 3 deletions src/review/guardrail-config.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,59 @@
import type { RepositorySettings } from "../types";

export const CONFIG_AS_CODE_GUARDRAIL_GLOBS = [
".gittensory.yml",
".gittensory.yaml",
".gittensory.json",
".github/gittensory.yml",
".github/gittensory.yaml",
".github/gittensory.json",
"**/codecov.yml",
"**/codecov.yaml",
"**/.codecov.yml",
];

export const WORKFLOW_AND_RUNTIME_GUARDRAIL_GLOBS = [
".github/workflows/**",
"scripts/**",
"wrangler.jsonc",
"src/selfhost/**",
];

export const ENGINE_DECISION_GUARDRAIL_GLOBS = [
"src/rules/**",
"src/services/**",
"src/settings/agent-actions.ts",
"src/settings/agent-execution.ts",
"src/settings/agent-sweep.ts",
"src/settings/autonomy.ts",
"src/queue/**",
"src/github/pr-actions.ts",
"src/github/app.ts",
"src/github/backfill.ts",
"src/scoring/**",
"src/auth/**",
"src/review/safety.ts",
"src/review/guardrail-config.ts",
"src/review/cutover-gate.ts",
"src/review/linked-issue-hard-rules.ts",
"src/review/outcomes-wire.ts",
];

export const DEFAULT_HARD_GUARDRAIL_GLOBS = [
...CONFIG_AS_CODE_GUARDRAIL_GLOBS,
...WORKFLOW_AND_RUNTIME_GUARDRAIL_GLOBS,
...ENGINE_DECISION_GUARDRAIL_GLOBS,
];

/**
* Resolve hard-guardrail path globs from the already-effective repo settings. Path holds are config-as-code only:
* omitted/null settings mean no path guardrails, and arrays replace lower layers wholesale.
* Resolve hard-guardrail path globs from the already-effective repo settings. Built-in config-as-code,
* workflow/runtime, and engine decision guardrails are invariants; repo settings may only add globs.
*/
export function resolveHardGuardrailGlobs(
settings: Pick<RepositorySettings, "hardGuardrailGlobs"> | null | undefined,
): string[] {
const configured = settings?.hardGuardrailGlobs;
return Array.isArray(configured) ? [...configured] : [];
return Array.from(
new Set([...DEFAULT_HARD_GUARDRAIL_GLOBS, ...(Array.isArray(configured) ? configured : [])]),
);
}
27 changes: 15 additions & 12 deletions test/unit/guardrail-config.test.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,29 @@
import { describe, expect, it } from "vitest";
import { resolveHardGuardrailGlobs } from "../../src/review/guardrail-config";
import {
DEFAULT_HARD_GUARDRAIL_GLOBS,
resolveHardGuardrailGlobs,
} from "../../src/review/guardrail-config";

describe("resolveHardGuardrailGlobs", () => {
it("does not invent path guardrails when effective settings omit hardGuardrailGlobs", () => {
expect(resolveHardGuardrailGlobs(undefined)).toEqual([]);
expect(resolveHardGuardrailGlobs(null)).toEqual([]);
expect(resolveHardGuardrailGlobs({})).toEqual([]);
expect(resolveHardGuardrailGlobs({ hardGuardrailGlobs: null })).toEqual([]);
it("uses invariant guardrails when effective settings omit hardGuardrailGlobs", () => {
expect(resolveHardGuardrailGlobs(undefined)).toEqual(DEFAULT_HARD_GUARDRAIL_GLOBS);
expect(resolveHardGuardrailGlobs(null)).toEqual(DEFAULT_HARD_GUARDRAIL_GLOBS);
expect(resolveHardGuardrailGlobs({})).toEqual(DEFAULT_HARD_GUARDRAIL_GLOBS);
expect(resolveHardGuardrailGlobs({ hardGuardrailGlobs: null })).toEqual(DEFAULT_HARD_GUARDRAIL_GLOBS);
});

it("returns a clone of the configured guardrail globs", () => {
const configured = ["src/settings/**", ".github/workflows/**"];
it("adds configured guardrail globs without allowing them to replace invariants", () => {
const configured = ["src/custom/**", ".github/workflows/**"];
const resolved = resolveHardGuardrailGlobs({ hardGuardrailGlobs: configured });

expect(resolved).toEqual(configured);
expect(resolved).toEqual([...DEFAULT_HARD_GUARDRAIL_GLOBS, "src/custom/**"]);
expect(resolved).not.toBe(configured);

resolved.push("mutated/**");
expect(configured).toEqual(["src/settings/**", ".github/workflows/**"]);
expect(configured).toEqual(["src/custom/**", ".github/workflows/**"]);
});

it("preserves an explicit empty list as no path guardrails", () => {
expect(resolveHardGuardrailGlobs({ hardGuardrailGlobs: [] })).toEqual([]);
it("keeps invariant guardrails when configured globs are explicitly empty", () => {
expect(resolveHardGuardrailGlobs({ hardGuardrailGlobs: [] })).toEqual(DEFAULT_HARD_GUARDRAIL_GLOBS);
});
});
12 changes: 6 additions & 6 deletions test/unit/predicted-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -451,20 +451,20 @@ describe("buildPredictedGateVerdict", () => {
expect(result.blockers).toHaveLength(0);
});

it("does NOT predict a guardrail hold when hardGuardrailGlobs is omitted", () => {
it("predicts a guardrail hold for invariant guardrails when hardGuardrailGlobs is omitted", () => {
const result = verdict({ gate: { duplicates: "block" }, changedPaths: [".github/workflows/ci.yml"] });
expect(result.conclusion).toBe("success");
expect(result.warnings.some((w) => w.code === "guardrail_hold")).toBe(false);
expect(result.conclusion).toBe("neutral");
expect(result.warnings.some((w) => w.code === "guardrail_hold")).toBe(true);
});

it("does NOT predict a guardrail hold when hardGuardrailGlobs is explicitly empty", () => {
it("REGRESSION: predicts a guardrail hold for invariant guardrails when hardGuardrailGlobs is explicitly empty", () => {
const result = verdict({
gate: { duplicates: "block" },
manifestExtra: { settings: { hardGuardrailGlobs: [] } },
changedPaths: [".github/workflows/ci.yml"],
});
expect(result.conclusion).toBe("success");
expect(result.warnings.some((w) => w.code === "guardrail_hold")).toBe(false);
expect(result.conclusion).toBe("neutral");
expect(result.warnings.some((w) => w.code === "guardrail_hold")).toBe(true);
});

it("does NOT predict a guardrail hold for an ordinary changed path", () => {
Expand Down
5 changes: 4 additions & 1 deletion test/unit/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11366,7 +11366,10 @@ describe("queue processors", () => {
if (/\/pulls\/\d+(?:\?|$)/.test(url) && method === "GET" && !url.includes(`/pulls/${prNumber}/`)) {
return Response.json({ number: prNumber, state: "open", user: { login: "contributor" }, head: { sha: "sha1" }, base: { ref: "main", sha: "base" }, mergeable_state: "clean", labels: [] });
}
if (url.includes(`/pulls/${prNumber}/files`)) return Response.json([{ filename: "src/queue/webhook-retry.ts", status: "modified", additions: 5, deletions: 0, changes: 5, patch: "@@\n+dedupe retries" }]);
// src/github/webhook.ts (not src/queue/**): this block tests the unlinked-issue guardrail specifically,
// and src/queue/** is one of ENGINE_DECISION_GUARDRAIL_GLOBS' built-in invariants (guardrail-config.ts) —
// a diff touching it would unconditionally hold regardless of this guardrail's own on/off setting.
if (url.includes(`/pulls/${prNumber}/files`)) return Response.json([{ filename: "src/github/webhook.ts", status: "modified", additions: 5, deletions: 0, changes: 5, patch: "@@\n+dedupe retries" }]);
if (url.includes(`/commits/sha1/check-runs`)) return Response.json({ total_count: 0, check_runs: [] });
if (url.includes(`/commits/sha1/status`)) return Response.json({ state: "success", statuses: [{ context: "ci/build", state: "success", description: "ok" }] });
if (url.includes(`/commits/sha1/check-suites`)) return Response.json({ check_suites: [] });
Expand Down