# UOK Gate Call-Site Migration Template (ADR-0075 / R079)

Migrate inline `new UokGateRunner()` call sites to `UokGateRegistry` +
`ctxFactory` pattern. The process singleton in `uok/gate-registry.js` replaces
ad-hoc runner construction and enables cross-process gate health queries.

---

## 4-Step Pattern

### Step 1 — Read the inline block

Find the `new UokGateRunner()` instantiation, the `runner.register({id, type,
execute: async () => { ... }})` call, and note every local variable the closure
captures. These become the `ctx` fields you will supply at run time.

### Step 2 — Lift to module scope

Define a top-level `const` for the gate object. The `execute` function receives
`ctx` as its first argument; replace every closure reference with a `ctx.*`
read:

```js
// module scope — defined once, not inside any function
const myGate = {
  id: "my-gate-id",
  type: "verification",          // must be a valid UOK_GATE_TYPES value
  execute: async (ctx) => ({
    outcome: ctx.someFlag ? "manual-attention" : "pass",
    failureClass: ctx.someFlag ? "manual-attention" : "none",
    rationale: `gate result: ${ctx.someValue}`,
    findings: ctx.someFlag ? ctx.details ?? "" : "",
  }),
};
```

### Step 3 — Register with a ctxFactory (or inline override)

Two sub-cases:

**A. Per-call state (most common):** supply the captured locals as a
`ctxOverride` object to `registry.run()`. No `ctxFactory` argument needed when
`register()` is called:

```js
const registry = getGateRegistry();
if (!registry.has(myGate.id)) {
  registry.register(myGate);            // no ctxFactory — state comes from override
}
await registry.run(myGate.id, {
  someFlag: localFlag,                   // previously-closed-over state
  someValue: localValue,
  // plus standard runner ctx fields:
  basePath,
  traceId: opts?.traceId ?? `my-trace`,
  turnId: opts?.turnId ?? `my-turn`,
  milestoneId, sliceId, unitType, unitId,
});
```

**B. Stable per-module state:** if the gate always needs the same context
object (e.g. a fixed config block), pass a `ctxFactory` to `register()` so
callers never have to supply it:

```js
registry.register(myGate, () => ({ stableConfig }));
await registry.run(myGate.id, {});   // ctxFactory merged automatically
```

`registry.run(id, override)` merges as `{ ...ctxFactory(), ...override }`, so
override values always win.

### Step 4 — Remove the original inline block

Delete the `new UokGateRunner()` instantiation, the `runner.register(...)` call,
and the `await runner.run(...)` call. Import `getGateRegistry` from
`"../uok/gate-registry.js"` and remove the `UokGateRunner` import if it is no
longer used.

---

## Worked Example — `tools/validate-milestone.js`

### Before (199 lines)

```js
import { UokGateRunner } from "../uok/gate-runner.js";
// ...inside handleValidateMilestone:
if (gatesEnabled) {
  try {
    const gateRunner = new UokGateRunner();
    const nonPassVerdict = params.verdict !== "pass";
    gateRunner.register({
      id: "milestone-validation-gates",
      type: "verification",
      execute: async () => ({
        outcome: nonPassVerdict ? "manual-attention" : "pass",
        failureClass: nonPassVerdict ? "manual-attention" : "none",
        rationale: `milestone validation verdict: ${params.verdict}`,
        findings: nonPassVerdict
          ? [params.verdictRationale, params.remediationPlan ?? ""].filter(Boolean).join("\n")
          : "",
      }),
    });
    await gateRunner.run("milestone-validation-gates", { basePath, traceId, ... });
  } catch (err) { logWarning(...); }
}
```

The closure captured `params.verdict`, `params.verdictRationale`,
`params.remediationPlan`, and the derived `nonPassVerdict` flag — all call-time
values that differed on every invocation.

### After (222 lines)

```js
import { getGateRegistry } from "../uok/gate-registry.js";

// module scope
const milestoneValidationGatesGate = {
  id: "milestone-validation-gates",
  type: "verification",
  execute: async (ctx) => {
    const nonPassVerdict = ctx.verdict !== "pass";
    return {
      outcome: nonPassVerdict ? "manual-attention" : "pass",
      failureClass: nonPassVerdict ? "manual-attention" : "none",
      rationale: `milestone validation verdict: ${ctx.verdict}`,
      findings: nonPassVerdict
        ? [ctx.verdictRationale, ctx.remediationPlan ?? ""].filter(Boolean).join("\n")
        : "",
    };
  },
};

// inside handleValidateMilestone:
if (gatesEnabled) {
  try {
    const registry = getGateRegistry();
    if (!registry.has(milestoneValidationGatesGate.id)) {
      registry.register(milestoneValidationGatesGate);
    }
    await registry.run(milestoneValidationGatesGate.id, {
      verdict: params.verdict,
      verdictRationale: params.verdictRationale,
      remediationPlan: params.remediationPlan,
      basePath, traceId: opts?.traceId ?? `...`, ...
    });
  } catch (err) { logWarning(...); }
}
```

The gate object is now registered once (guarded by `registry.has()`). The
previously-closed-over locals are passed as a plain object to `registry.run()`,
where they merge with the ctxFactory output (none here) before reaching
`execute(ctx)`.

---

## Per-Gate Scope Checklist (5 remaining inline gates)

| Gate id | File | Captured locals | Notes |
|---------|------|-----------------|-------|
| `unit-verification-gate` | `auto/phases-finalize.js` | `deps`, `s`, `ctx`, `pi`, `pauseAuto` | Complex; `ctx` is the outer phase context — rename to `phaseCtx` in gate's `execute(ctx)` to avoid shadowing |
| `milestone-validation-post-check` | `uok/auto-verification.js` | `persistGate` args from `runValidateMilestonePostCheck` | Straightforward; all are primitive strings/booleans |
| `verification-gate` | `uok/auto-verification.js` | local verification result + helper formatting state | Multiple gates in same file — see Pitfall section |
| `post-execution-checks` | `uok/auto-verification.js` | `postExecResult`, strict-mode prefs, derived blocking state | Three gates in same file — same Pitfall applies |
| `planning-flow-gate` | `guided-flow.js` | `persistGate` args | Clean; same pattern as milestone-validation-post-check |

---

## When to Skip ctxFactory

If the gate's `execute()` needs no context at all (e.g. it reads only DB or
env state), register without a factory and run with an empty override:

```js
registry.register(myStatelessGate);   // no ctxFactory
await registry.run(myStatelessGate.id, { basePath, traceId, turnId });
```

`uok/gate-registry-bootstrap.js` uses this form for `driftDetectionGate`, which
sources everything from the DB.

---

## Pitfall: Multiple Call Sites, Same Gate Id, Different State

`uok/auto-verification.js` contains three distinct gate registrations
(`verification-gate`, `milestone-validation-post-check`,
`post-execution-checks`). Each has its own unique id, so there is no collision.

However, if a future refactor discovers two call sites in the SAME file that
run the SAME gate id with different state shapes, choose one of:

**Option A — Two distinct gate ids** (preferred when logic differs):

```js
const myGateA = { id: "my-gate-pass-path", type: "verification", execute: async (ctx) => ... };
const myGateB = { id: "my-gate-fail-path", type: "verification", execute: async (ctx) => ... };
```

Each id carries its own circuit-breaker state and audit trail — cleaner
semantics at the cost of a registry entry per variant.

**Option B — Single id, discriminator field in ctx** (preferred when logic is
identical but input differs):

```js
const myGate = {
  id: "my-gate",
  type: "verification",
  execute: async (ctx) => {
    if (ctx.variant === "foo") { ... }
    else { ... }
  },
};
// call site A:
await registry.run("my-gate", { variant: "foo", ... });
// call site B:
await registry.run("my-gate", { variant: "bar", ... });
```

Trade-off: Option A gives separate circuit breakers (fail on path A does not
suppress path B). Option B shares a circuit breaker — one pathological failure
mode can block the other path. Prefer Option A when the two paths have
meaningfully different failure domains.
