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
30 changes: 14 additions & 16 deletions src/review/selftune-wire.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,17 +22,15 @@
// The mapping is deliberately conservative: it only ever populates the would-MERGE side of the matrix, so the
// advisor can only ever recommend a TIGHTENING (raise the floor) or a no-op — never a loosening.
//
// CONFIG-APPLICATION — DEFERRED (NOT wired here), by design and per the convergence task:
// The ported override model is `confidenceFloor` (an auto-merge confidence floor in [0,1]) + `scopeCap`.
// Gittensory's gate has NEITHER concept — its live tunables are `qualityGateMinScore` / `slopGateMinScore`
// (integer score thresholds) and gate MODES (off/advisory/block), resolved via resolveRepositorySettings →
// GateCheckPolicy. There is no field a promoted `confidenceFloor`/`scopeCap` override maps onto, and
// gittensory's native accuracy signal measures gate FALSE POSITIVES (blocked-then-merged → a LOOSENING
// direction). Wiring a promoted override into the live gate is therefore engine-coupled AND points the wrong
// way, so it is intentionally NOT read by the gate yet: this module does the MIGRATION + shadow-soak + audit
// + recommendation recording, and a promoted override sits inert in `tunables_overrides`. Reading it into the
// live gate-config resolution is a noted follow-up that must not risk loosening the gate. (See loadOverride /
// resolveRepositorySettings — the seam exists; closing it needs a gittensory-native tightening tunable.)
// CONFIG-APPLICATION — WIRED (live read-back, tightening-only):
// The ported override model is `confidenceFloor` (a proceed-confidence floor in [0,1]) + `scopeCap`. The live
// read-back lives in resolveRepositorySettings → `applySelfTuneOverrideToSettings`, gated by the SAME default-OFF
// GITTENSORY_REVIEW_SELFTUNE flag: it translates a promoted `confidenceFloor` into gittensory's NATIVE readiness
// tunable by RAISING an EXISTING `qualityGateMinScore` to `round(confidenceFloor * 100)` via a `max()`. By
// construction this can ONLY tighten — it never CREATES a readiness gate the operator didn't set, and never
// LOWERS one — so the always-tightening recommendation (this module only ever populates the would-merge error
// side, so the advisor can only raise the floor) reaches the live gate with no risk of loosening it. Flag-OFF
// (default) the override is never read and settings are byte-identical. (See applySelfTuneOverrideToSettings.)

import { listRepositories } from "../db/repositories";
import { isAgentConfigured } from "../settings/autonomy";
Expand All @@ -48,11 +46,11 @@ export function isSelfTuneEnabled(env: { GITTENSORY_REVIEW_SELFTUNE?: string | u
return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_SELFTUNE ?? "");
}

/** The project's base confidence floor the tightening direction is judged against. Gittensory has no live
* confidenceFloor tunable (config-application is deferred), so a no-override project starts from an UNSET
* base — the apply path treats "no live floor" as the loosest state, so any positive floor recommendation is
* strictly tightening (it can only HOLD more, never add a bad auto-merge). Exposed as a constant so the
* config-application follow-up has a single place to source the real per-project base from. */
/** The project's base confidence floor the tightening direction is judged against IN THE SOAK. Gittensory has no
* live `confidenceFloor` tunable (the live read-back instead RAISES `qualityGateMinScore` — see
* applySelfTuneOverrideToSettings), so a no-override project starts the soak from an UNSET base — the apply path
* treats "no live floor" as the loosest state, so any positive floor recommendation is strictly tightening (it
* can only HOLD more, never add a bad auto-merge). */
export const SELFTUNE_BASE_CONFIDENCE_FLOOR = 0;

/**
Expand Down
35 changes: 33 additions & 2 deletions src/settings/repository-settings.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,44 @@
import { getRepositorySettings } from "../db/repositories";
import { loadOverride, type StorageEnv } from "../review/auto-apply";
import { resolveEffectiveSettings } from "../signals/focus-manifest";
import { loadRepoFocusManifest } from "../signals/focus-manifest-loader";
import type { RepositorySettings } from "../types";

/** Effective repository settings: DB values overlaid with `.gittensory.yml` (config-as-code). */
/** Default-OFF self-tune flag (mirrors selftune-wire's `isSelfTuneEnabled`; inlined here to avoid a
* selftune-wire → repository-settings → selftune-wire import cycle). */
function selfTuneFlagOn(env: { GITTENSORY_REVIEW_SELFTUNE?: string | undefined }): boolean {
return /^(1|true|yes|on)$/i.test(env.GITTENSORY_REVIEW_SELFTUNE ?? "");
}

/** PURE: overlay a promoted (always TIGHTENING-only) self-tune override onto resolved settings. The auto-tune's
* `confidenceFloor` [0,1] is translated to a readiness-score floor [0,100] and applied as a `max()`, so it can
* ONLY RAISE an EXISTING `qualityGateMinScore` — never CREATE one (a repo with no readiness threshold keeps
* none, respecting the operator's choice) and never LOWER it. No override / no floor / no existing threshold /
* a floor at-or-below the current ⇒ settings are returned unchanged. This is the live read-back of the loop
* that `auto-apply.ts` shadow-soaks + promotes into `tunables_overrides` (the read-back was previously deferred). */
export function applySelfTuneOverrideToSettings(
settings: RepositorySettings,
override: { confidenceFloor?: number | undefined } | null,
): RepositorySettings {
const floor = override?.confidenceFloor;
if (floor === undefined) return settings; // no override / no promoted floor
const current = settings.qualityGateMinScore;
if (typeof current !== "number") return settings; // never CREATE a readiness gate the operator didn't set
const floorScore = Math.max(0, Math.min(100, Math.round(floor * 100)));
return floorScore > current ? { ...settings, qualityGateMinScore: floorScore } : settings; // raise only
}

/** Effective repository settings: DB values overlaid with `.gittensory.yml` (config-as-code), then — when the
* self-improvement loop is enabled (`GITTENSORY_REVIEW_SELFTUNE`, default OFF) — with the repo's promoted,
* soak-passed, tightening-only auto-tune override. Flag-OFF (default) ⇒ no override read, byte-identical to before. */
export async function resolveRepositorySettings(env: Env, repoFullName: string): Promise<RepositorySettings> {
const [dbSettings, manifest] = await Promise.all([
getRepositorySettings(env, repoFullName),
loadRepoFocusManifest(env, repoFullName),
]);
return resolveEffectiveSettings(dbSettings, manifest);
const effective = resolveEffectiveSettings(dbSettings, manifest);
if (!selfTuneFlagOn(env)) return effective;
// loadOverride is internally fail-safe (returns null on a DB blip), so this never breaks settings resolution.
const override = await loadOverride(env as unknown as StorageEnv, repoFullName);
return applySelfTuneOverrideToSettings(effective, override);
}
52 changes: 52 additions & 0 deletions test/unit/selftune-readback.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { describe, expect, it } from "vitest";
import { upsertRepositorySettings } from "../../src/db/repositories";
import { writeLiveOverride, type StorageEnv } from "../../src/review/auto-apply";
import { applySelfTuneOverrideToSettings, resolveRepositorySettings } from "../../src/settings/repository-settings";
import type { RepositorySettings } from "../../src/types";
import { createTestEnv } from "../helpers/d1";

// The promoted override is ALWAYS a tightening (selftune-wire only ever populates the would-merge error side),
// so the read-back must only ever RAISE an existing readiness threshold — never create or lower one.
const baseSettings = { qualityGateMinScore: 50 } as RepositorySettings;

describe("applySelfTuneOverrideToSettings — tightening-only live read-back (#self-improve)", () => {
it("RAISES an existing readiness threshold to the promoted floor (confidenceFloor 0.7 → 70)", () => {
expect(applySelfTuneOverrideToSettings(baseSettings, { confidenceFloor: 0.7 }).qualityGateMinScore).toBe(70);
});

it("NEVER lowers — a floor at or below the current threshold is a no-op (same object back)", () => {
expect(applySelfTuneOverrideToSettings(baseSettings, { confidenceFloor: 0.5 })).toBe(baseSettings); // 50 ≯ 50
expect(applySelfTuneOverrideToSettings(baseSettings, { confidenceFloor: 0.3 })).toBe(baseSettings); // 30 < 50
});

it("NEVER creates a gate the operator didn't set (null threshold ⇒ unchanged)", () => {
const noGate = { qualityGateMinScore: null } as RepositorySettings;
expect(applySelfTuneOverrideToSettings(noGate, { confidenceFloor: 0.9 })).toBe(noGate);
});

it("no override / no promoted floor ⇒ unchanged", () => {
expect(applySelfTuneOverrideToSettings(baseSettings, null)).toBe(baseSettings);
expect(applySelfTuneOverrideToSettings(baseSettings, {})).toBe(baseSettings);
});
});

describe("resolveRepositorySettings — self-tune override overlay (flag-gated)", () => {
const repo = "acme/widgets";
async function seed(env: Env): Promise<void> {
await env.DB.prepare("INSERT INTO repositories (full_name, owner, name, is_installed, is_registered) VALUES (?, 'acme', 'widgets', 1, 1)").bind(repo).run();
await upsertRepositorySettings(env, { repoFullName: repo, qualityGateMinScore: 50 });
await writeLiveOverride(env as unknown as StorageEnv, repo, { confidenceFloor: 0.7 });
}

it("flag ON: overlays the promoted tightening override (50 → 70)", async () => {
const env = createTestEnv({ GITTENSORY_REVIEW_SELFTUNE: "true" });
await seed(env);
expect((await resolveRepositorySettings(env, repo)).qualityGateMinScore).toBe(70);
});

it("flag OFF (default): the override is never read — settings stay byte-identical (50)", async () => {
const env = createTestEnv();
await seed(env);
expect((await resolveRepositorySettings(env, repo)).qualityGateMinScore).toBe(50);
});
});
Loading