feat(detectors): gate-deadlock-classifier — Wiggums detector for R074 self-deadlock
The R074 inlineRuntimeGate refused inline dispatch for M048/S05 reassess-roadmap
because R020 and R066 are still 'active' — but those slices ARE the work that
validates R066. Autonomous mode stopped with no way to escape. Filed earlier as
sf-mpa4f9k1-jm01rc.
This detector classifies the pattern at runtime:
parseGateRefusal(rationale)
extracts gateId + refused requirement ids from gate-refusal text
matching shape "[gate-id] ... R020=active R066=active ..."
detectGateDeadlock(ctx, options)
ctx.gateRefusals: recent gate refusal events ({rationale, unitType, unitId})
ctx.requirementCoverageByMilestone: milestone -> R-ids in its DoD/coverage
ctx.resolveMilestoneId: optional unit -> milestone resolver
(default: strip after '/', require M-prefix)
Returns { stuck, reason: "gate-deadlock", signature: {
gateId, deadlockedRequirements, refusedUnits, examples, suggestedAction
}} when any refused unit's milestone coverage overlaps the gate's refused
requirements. Per-gateId throttle prevents repeat firings within 60s.
gateDeadlockClassifierGate
UokGate (type=verification per ADR-0075) wrapping the detector for
integration into periodicDetectorSweepGate + post-finalize sweeps.
Registered in uok/gate-registry-bootstrap.js between inlineRuntimeGate and the
existing detector chain. Also re-exported from detectors/index.js for the
common detector import surface.
Test coverage:
- parseGateRefusal: 5 cases (inline shape, dedup, missing reqs, missing gate, empty)
- detectGateDeadlock: 7 cases (empty input, fire-on-overlap, no-overlap,
empty coverage, throttle, custom resolver,
examples cap)
- UokGate wrapper: 3 cases (contract shape, pass, fail-with-findings)
- Threshold export sanity: 1 case
16/16 tests pass.
The wiring from autonomous-loop output (where gate refusals are emitted) into
the detector's gateRefusals input is a follow-up — this commit lands the
detector with a stable contract and tests it can be wired against.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
acd907fec2
commit
ab2c996866
4 changed files with 398 additions and 0 deletions
|
|
@ -0,0 +1,198 @@
|
||||||
|
/**
|
||||||
|
* gate-deadlock-classifier.js — detect closed-loop UokGate refusals.
|
||||||
|
*
|
||||||
|
* Purpose: catch the pattern where a UokGate refuses dispatch on a unit
|
||||||
|
* whose purpose would advance the very requirement validation the gate
|
||||||
|
* is waiting for. SF cannot lift a gate that's blocking the work that
|
||||||
|
* would lift the gate.
|
||||||
|
*
|
||||||
|
* Concrete trigger (2026-05-17 incident): R074 inlineRuntimeGate refused
|
||||||
|
* inline dispatch for M048/S05 reassess-roadmap because R020 and R066
|
||||||
|
* were still 'active' — but reassess-roadmap + M048 slices ARE the work
|
||||||
|
* that validates R066. Autonomous mode stopped permanently until manual
|
||||||
|
* operator intervention.
|
||||||
|
*
|
||||||
|
* Consumer: periodic-runner.js default detector list and the autonomous
|
||||||
|
* loop's post-dispatch sweep. Filed as sf-mpa4f9k1-jm01rc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimum number of distinct refused units sharing the same gate before
|
||||||
|
* firing — guards against a single transient refusal triggering a
|
||||||
|
* false-positive deadlock report.
|
||||||
|
*/
|
||||||
|
export const DEADLOCK_REFUSAL_THRESHOLD = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse a gate refusal rationale into { gateId, refusedRequirements }.
|
||||||
|
*
|
||||||
|
* Purpose: extract the requirement identifiers a gate is waiting on so
|
||||||
|
* the detector can cross-reference against the refused unit's milestone
|
||||||
|
* coverage. The rationale format mirrors the existing inlineRuntimeGate
|
||||||
|
* shape: "[<gate-id>] ... R020=active R066=active ...".
|
||||||
|
*
|
||||||
|
* Returns null if the input doesn't match the expected refusal shape.
|
||||||
|
*
|
||||||
|
* @param {string} rationale
|
||||||
|
* @returns {{ gateId: string, refusedRequirements: string[] } | null}
|
||||||
|
*/
|
||||||
|
export function parseGateRefusal(rationale) {
|
||||||
|
if (typeof rationale !== "string" || rationale.length === 0) return null;
|
||||||
|
const gateMatch = rationale.match(/\[([a-z][a-z0-9-]+(?:-gate)?)\]/);
|
||||||
|
if (!gateMatch) return null;
|
||||||
|
const reqMatches = rationale.match(/\bR\d{2,}\b(?==active|=pending|=deferred)/g);
|
||||||
|
if (!reqMatches || reqMatches.length === 0) return null;
|
||||||
|
const seen = new Set();
|
||||||
|
const refusedRequirements = [];
|
||||||
|
for (const id of reqMatches) {
|
||||||
|
if (!seen.has(id)) {
|
||||||
|
seen.add(id);
|
||||||
|
refusedRequirements.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { gateId: gateMatch[1], refusedRequirements };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detect closed-loop gate refusals.
|
||||||
|
*
|
||||||
|
* Purpose: classify the autonomous-loop halt pattern where the refusing
|
||||||
|
* gate is waiting on requirements that the refused unit would advance.
|
||||||
|
*
|
||||||
|
* Inputs (ctx):
|
||||||
|
* - gateRefusals?: Array<{ rationale, unitType?, unitId?, ts? }>
|
||||||
|
* - requirementCoverageByMilestone?: Record<string, string[]>
|
||||||
|
* (milestone id → list of R-ids in its definition_of_done / coverage)
|
||||||
|
* - resolveMilestoneId?: (unitId) => string | null
|
||||||
|
* (resolver for unitId → owning milestone, defaults to "M\d+" prefix)
|
||||||
|
* - now?: number — current time for throttling
|
||||||
|
* - throttleState?: Map<string, number> — per-gateId last-fired ms
|
||||||
|
* - options?: { throttleMs?: number, threshold?: number }
|
||||||
|
*
|
||||||
|
* Returns standard detector shape: { stuck, reason, signature }.
|
||||||
|
*
|
||||||
|
* @returns {{ stuck: boolean, reason: string, signature: object }}
|
||||||
|
*/
|
||||||
|
export function detectGateDeadlock(ctx = {}, options = {}) {
|
||||||
|
const refusals = Array.isArray(ctx.gateRefusals) ? ctx.gateRefusals : [];
|
||||||
|
if (refusals.length === 0) {
|
||||||
|
return { stuck: false, reason: "", signature: {} };
|
||||||
|
}
|
||||||
|
const coverageMap =
|
||||||
|
ctx.requirementCoverageByMilestone &&
|
||||||
|
typeof ctx.requirementCoverageByMilestone === "object"
|
||||||
|
? ctx.requirementCoverageByMilestone
|
||||||
|
: {};
|
||||||
|
const resolveMilestone =
|
||||||
|
typeof ctx.resolveMilestoneId === "function"
|
||||||
|
? ctx.resolveMilestoneId
|
||||||
|
: defaultResolveMilestone;
|
||||||
|
const now = options.now ?? ctx.now ?? Date.now();
|
||||||
|
const throttleMs = options.throttleMs ?? 60_000;
|
||||||
|
const threshold = options.threshold ?? DEADLOCK_REFUSAL_THRESHOLD;
|
||||||
|
const throttleState =
|
||||||
|
ctx.throttleState instanceof Map ? ctx.throttleState : null;
|
||||||
|
|
||||||
|
// Group refusals by gateId and accumulate the set of refused unit/milestone
|
||||||
|
// pairs whose milestone coverage overlaps with the gate's refused
|
||||||
|
// requirements. A deadlock fires when threshold ≥ DEADLOCK_REFUSAL_THRESHOLD
|
||||||
|
// distinct overlapping unit/milestone pairs are observed.
|
||||||
|
const byGate = new Map();
|
||||||
|
for (const refusal of refusals) {
|
||||||
|
const parsed = parseGateRefusal(refusal?.rationale);
|
||||||
|
if (!parsed) continue;
|
||||||
|
const milestoneId = resolveMilestone(refusal?.unitId ?? "");
|
||||||
|
if (!milestoneId) continue;
|
||||||
|
const milestoneCoverage = coverageMap[milestoneId];
|
||||||
|
if (!Array.isArray(milestoneCoverage) || milestoneCoverage.length === 0)
|
||||||
|
continue;
|
||||||
|
const overlap = parsed.refusedRequirements.filter((id) =>
|
||||||
|
milestoneCoverage.includes(id),
|
||||||
|
);
|
||||||
|
if (overlap.length === 0) continue;
|
||||||
|
let bucket = byGate.get(parsed.gateId);
|
||||||
|
if (!bucket) {
|
||||||
|
bucket = {
|
||||||
|
gateId: parsed.gateId,
|
||||||
|
deadlockedRequirements: new Set(),
|
||||||
|
refusedUnits: new Set(),
|
||||||
|
examples: [],
|
||||||
|
};
|
||||||
|
byGate.set(parsed.gateId, bucket);
|
||||||
|
}
|
||||||
|
for (const id of overlap) bucket.deadlockedRequirements.add(id);
|
||||||
|
bucket.refusedUnits.add(`${milestoneId}/${refusal?.unitId ?? "?"}`);
|
||||||
|
if (bucket.examples.length < 3) {
|
||||||
|
bucket.examples.push({
|
||||||
|
unitId: refusal?.unitId ?? null,
|
||||||
|
unitType: refusal?.unitType ?? null,
|
||||||
|
milestoneId,
|
||||||
|
overlap,
|
||||||
|
ts: refusal?.ts ?? null,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const bucket of byGate.values()) {
|
||||||
|
if (bucket.refusedUnits.size < threshold) continue;
|
||||||
|
if (throttleState) {
|
||||||
|
const last = throttleState.get(`gate-deadlock:${bucket.gateId}`);
|
||||||
|
if (typeof last === "number" && now - last < throttleMs) continue;
|
||||||
|
throttleState.set(`gate-deadlock:${bucket.gateId}`, now);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
stuck: true,
|
||||||
|
reason: "gate-deadlock",
|
||||||
|
signature: {
|
||||||
|
gateId: bucket.gateId,
|
||||||
|
deadlockedRequirements: [...bucket.deadlockedRequirements],
|
||||||
|
refusedUnits: [...bucket.refusedUnits],
|
||||||
|
examples: bucket.examples,
|
||||||
|
suggestedAction:
|
||||||
|
"file self-feedback (kind=gate-deadlock-detected, severity=high); " +
|
||||||
|
"consider audited bypass via gate-specific env or spawn-mode retry until " +
|
||||||
|
"deadlocked requirements validate",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stuck: false, reason: "", signature: {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Default unitId → milestoneId resolver: strip everything from first '/'. */
|
||||||
|
function defaultResolveMilestone(unitId) {
|
||||||
|
if (typeof unitId !== "string" || unitId.length === 0) return null;
|
||||||
|
const idx = unitId.indexOf("/");
|
||||||
|
if (idx <= 0) return unitId.startsWith("M") ? unitId : null;
|
||||||
|
const head = unitId.slice(0, idx);
|
||||||
|
return head.startsWith("M") ? head : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the gate-deadlock classifier as a UOK verification gate.
|
||||||
|
*
|
||||||
|
* Purpose: surface the deadlock via the common gate contract so it can
|
||||||
|
* be wired into periodicDetectorSweepGate and post-finalize sweeps.
|
||||||
|
*
|
||||||
|
* Consumer: gate-registry-bootstrap.js and tests.
|
||||||
|
*/
|
||||||
|
export const gateDeadlockClassifierGate = {
|
||||||
|
id: "gate-deadlock-classifier",
|
||||||
|
type: "verification",
|
||||||
|
async execute(ctx = {}) {
|
||||||
|
const result = detectGateDeadlock(ctx, ctx.options ?? {});
|
||||||
|
if (result.stuck) {
|
||||||
|
return {
|
||||||
|
outcome: "fail",
|
||||||
|
failureClass: "verification",
|
||||||
|
rationale: `gate-deadlock on ${result.signature.gateId}: refused units ${result.signature.refusedUnits.join(", ")} need ${result.signature.deadlockedRequirements.join(", ")} which the gate is waiting for`,
|
||||||
|
findings: result.signature,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
outcome: "pass",
|
||||||
|
failureClass: null,
|
||||||
|
rationale: "no gate-deadlock pattern",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -7,6 +7,7 @@
|
||||||
|
|
||||||
export { artifactFlapGate } from "./artifact-flap.js";
|
export { artifactFlapGate } from "./artifact-flap.js";
|
||||||
export { crashLoopGate } from "./crash-loop-classifier.js";
|
export { crashLoopGate } from "./crash-loop-classifier.js";
|
||||||
|
export { gateDeadlockClassifierGate } from "./gate-deadlock-classifier.js";
|
||||||
export { periodicDetectorSweepGate } from "./periodic-runner.js";
|
export { periodicDetectorSweepGate } from "./periodic-runner.js";
|
||||||
export { productionPlateauGate } from "./production-plateau.js";
|
export { productionPlateauGate } from "./production-plateau.js";
|
||||||
export { repeatedFeedbackKindGate } from "./repeated-feedback-kind.js";
|
export { repeatedFeedbackKindGate } from "./repeated-feedback-kind.js";
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,194 @@
|
||||||
|
/**
|
||||||
|
* detector-gate-deadlock-classifier.test.mjs — tests for the gate-deadlock
|
||||||
|
* Wiggums detector that fires when a UokGate refuses dispatch on a unit
|
||||||
|
* whose milestone coverage would advance the requirements the gate is
|
||||||
|
* waiting for.
|
||||||
|
*
|
||||||
|
* Covers parseGateRefusal helper, the detector function across the
|
||||||
|
* deadlock-pattern × purpose-match × throttle × empty-input matrix, and
|
||||||
|
* the UokGate wrapper contract per ADR-0075.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
DEADLOCK_REFUSAL_THRESHOLD,
|
||||||
|
detectGateDeadlock,
|
||||||
|
gateDeadlockClassifierGate,
|
||||||
|
parseGateRefusal,
|
||||||
|
} from "../detectors/gate-deadlock-classifier.js";
|
||||||
|
|
||||||
|
const INLINE_REFUSAL =
|
||||||
|
"[inline-runtime-gate] inline dispatch refused: R020=active R066=active both must be 'validated' or set SF_INLINE_DISPATCH=1 for audited bypass";
|
||||||
|
|
||||||
|
// ─── parseGateRefusal ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("parseGateRefusal", () => {
|
||||||
|
it("extracts gate id and refused requirements from inline refusal", () => {
|
||||||
|
const parsed = parseGateRefusal(INLINE_REFUSAL);
|
||||||
|
expect(parsed).not.toBeNull();
|
||||||
|
expect(parsed.gateId).toBe("inline-runtime-gate");
|
||||||
|
expect(parsed.refusedRequirements).toEqual(["R020", "R066"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dedupes repeated requirement ids", () => {
|
||||||
|
const parsed = parseGateRefusal(
|
||||||
|
"[some-gate] refused: R020=active R020=active R066=pending",
|
||||||
|
);
|
||||||
|
expect(parsed.refusedRequirements).toEqual(["R020", "R066"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when no requirement ids are present", () => {
|
||||||
|
expect(parseGateRefusal("[some-gate] generic failure")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null when no gate id bracket is present", () => {
|
||||||
|
expect(parseGateRefusal("R020=active R066=active no gate")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns null on empty / non-string input", () => {
|
||||||
|
expect(parseGateRefusal("")).toBeNull();
|
||||||
|
expect(parseGateRefusal(null)).toBeNull();
|
||||||
|
expect(parseGateRefusal(undefined)).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── detectGateDeadlock ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("detectGateDeadlock", () => {
|
||||||
|
it("returns not-stuck for empty refusal list", () => {
|
||||||
|
expect(detectGateDeadlock({ gateRefusals: [] })).toEqual({
|
||||||
|
stuck: false,
|
||||||
|
reason: "",
|
||||||
|
signature: {},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("fires when refused unit's milestone covers the deadlocked requirement", () => {
|
||||||
|
const result = detectGateDeadlock({
|
||||||
|
gateRefusals: [
|
||||||
|
{
|
||||||
|
rationale: INLINE_REFUSAL,
|
||||||
|
unitType: "reassess-roadmap",
|
||||||
|
unitId: "M048/S05",
|
||||||
|
ts: "2026-05-17T18:24:00Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requirementCoverageByMilestone: { M048: ["R066", "R078"] },
|
||||||
|
});
|
||||||
|
expect(result.stuck).toBe(true);
|
||||||
|
expect(result.reason).toBe("gate-deadlock");
|
||||||
|
expect(result.signature.gateId).toBe("inline-runtime-gate");
|
||||||
|
expect(result.signature.deadlockedRequirements).toEqual(["R066"]);
|
||||||
|
expect(result.signature.refusedUnits).toEqual(["M048/M048/S05"]);
|
||||||
|
expect(result.signature.suggestedAction).toMatch(/audited bypass/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT fire when refused unit's milestone covers none of the gate requirements", () => {
|
||||||
|
const result = detectGateDeadlock({
|
||||||
|
gateRefusals: [
|
||||||
|
{
|
||||||
|
rationale: INLINE_REFUSAL,
|
||||||
|
unitType: "execute-task",
|
||||||
|
unitId: "M010/S04/T01",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requirementCoverageByMilestone: { M010: ["R015"] },
|
||||||
|
});
|
||||||
|
expect(result.stuck).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does NOT fire when milestone coverage map is empty for the unit's milestone", () => {
|
||||||
|
const result = detectGateDeadlock({
|
||||||
|
gateRefusals: [
|
||||||
|
{ rationale: INLINE_REFUSAL, unitId: "M048/S05" },
|
||||||
|
],
|
||||||
|
requirementCoverageByMilestone: {},
|
||||||
|
});
|
||||||
|
expect(result.stuck).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throttles repeat firings for the same gate within throttleMs", () => {
|
||||||
|
const throttleState = new Map();
|
||||||
|
const ctx = {
|
||||||
|
gateRefusals: [
|
||||||
|
{ rationale: INLINE_REFUSAL, unitId: "M048/S05" },
|
||||||
|
],
|
||||||
|
requirementCoverageByMilestone: { M048: ["R066"] },
|
||||||
|
throttleState,
|
||||||
|
};
|
||||||
|
const first = detectGateDeadlock(ctx, { now: 1000 });
|
||||||
|
expect(first.stuck).toBe(true);
|
||||||
|
const second = detectGateDeadlock(ctx, { now: 5000 });
|
||||||
|
expect(second.stuck).toBe(false);
|
||||||
|
const third = detectGateDeadlock(ctx, { now: 1000 + 70_000 });
|
||||||
|
expect(third.stuck).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("custom resolveMilestoneId overrides the default M-prefix resolver", () => {
|
||||||
|
const result = detectGateDeadlock({
|
||||||
|
gateRefusals: [
|
||||||
|
{ rationale: INLINE_REFUSAL, unitId: "slice-xyz" },
|
||||||
|
],
|
||||||
|
requirementCoverageByMilestone: { CUSTOM: ["R020"] },
|
||||||
|
resolveMilestoneId: () => "CUSTOM",
|
||||||
|
});
|
||||||
|
expect(result.stuck).toBe(true);
|
||||||
|
expect(result.signature.deadlockedRequirements).toEqual(["R020"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("captures at most 3 example refusals in signature", () => {
|
||||||
|
const refusals = ["M048/S01", "M048/S02", "M048/S03", "M048/S04"].map(
|
||||||
|
(unitId) => ({ rationale: INLINE_REFUSAL, unitId }),
|
||||||
|
);
|
||||||
|
const result = detectGateDeadlock({
|
||||||
|
gateRefusals: refusals,
|
||||||
|
requirementCoverageByMilestone: { M048: ["R066"] },
|
||||||
|
});
|
||||||
|
expect(result.stuck).toBe(true);
|
||||||
|
expect(result.signature.examples.length).toBeLessThanOrEqual(3);
|
||||||
|
expect(result.signature.refusedUnits.length).toBe(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── gateDeadlockClassifierGate (UokGate wrapper, ADR-0075) ────────────────
|
||||||
|
|
||||||
|
describe("gateDeadlockClassifierGate", () => {
|
||||||
|
it("exports the ADR-0075 gate contract shape", () => {
|
||||||
|
expect(gateDeadlockClassifierGate.id).toBe("gate-deadlock-classifier");
|
||||||
|
expect(gateDeadlockClassifierGate.type).toBe("verification");
|
||||||
|
expect(typeof gateDeadlockClassifierGate.execute).toBe("function");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("execute returns pass when no deadlock pattern", async () => {
|
||||||
|
const result = await gateDeadlockClassifierGate.execute({
|
||||||
|
gateRefusals: [],
|
||||||
|
});
|
||||||
|
expect(result.outcome).toBe("pass");
|
||||||
|
expect(result.failureClass).toBeNull();
|
||||||
|
expect(result.rationale).toContain("no gate-deadlock");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("execute returns fail with structured findings when deadlock detected", async () => {
|
||||||
|
const result = await gateDeadlockClassifierGate.execute({
|
||||||
|
gateRefusals: [
|
||||||
|
{ rationale: INLINE_REFUSAL, unitId: "M048/S05" },
|
||||||
|
],
|
||||||
|
requirementCoverageByMilestone: { M048: ["R066"] },
|
||||||
|
});
|
||||||
|
expect(result.outcome).toBe("fail");
|
||||||
|
expect(result.failureClass).toBe("verification");
|
||||||
|
expect(result.rationale).toContain("inline-runtime-gate");
|
||||||
|
expect(result.rationale).toContain("R066");
|
||||||
|
expect(result.findings.gateId).toBe("inline-runtime-gate");
|
||||||
|
expect(result.findings.deadlockedRequirements).toEqual(["R066"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── threshold sanity ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
describe("threshold export", () => {
|
||||||
|
it("DEADLOCK_REFUSAL_THRESHOLD is a positive integer", () => {
|
||||||
|
expect(DEADLOCK_REFUSAL_THRESHOLD).toBeGreaterThanOrEqual(1);
|
||||||
|
expect(Number.isInteger(DEADLOCK_REFUSAL_THRESHOLD)).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -12,6 +12,7 @@ import { artifactFlapGate } from "../detectors/artifact-flap.js";
|
||||||
import { staleLockGate } from "../detectors/stale-lock.js";
|
import { staleLockGate } from "../detectors/stale-lock.js";
|
||||||
import { periodicDetectorSweepGate } from "../detectors/periodic-runner.js";
|
import { periodicDetectorSweepGate } from "../detectors/periodic-runner.js";
|
||||||
import { serverDirectionDriftGate } from "../detectors/server-direction-drift.js";
|
import { serverDirectionDriftGate } from "../detectors/server-direction-drift.js";
|
||||||
|
import { gateDeadlockClassifierGate } from "../detectors/gate-deadlock-classifier.js";
|
||||||
import { inlineRuntimeGate } from "./inline-runtime-gate.js";
|
import { inlineRuntimeGate } from "./inline-runtime-gate.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -45,5 +46,9 @@ registry.register(staleLockGate);
|
||||||
registry.register(serverDirectionDriftGate);
|
registry.register(serverDirectionDriftGate);
|
||||||
registry.register(periodicDetectorSweepGate);
|
registry.register(periodicDetectorSweepGate);
|
||||||
registry.register(inlineRuntimeGate);
|
registry.register(inlineRuntimeGate);
|
||||||
|
// gate-deadlock-classifier — fires when a UokGate refuses dispatch on a
|
||||||
|
// unit whose milestone coverage would advance the gate's deadlocked
|
||||||
|
// requirements. Closes the R074 self-deadlock pattern (sf-mpa4f9k1).
|
||||||
|
registry.register(gateDeadlockClassifierGate);
|
||||||
|
|
||||||
export { registry as gateRegistry };
|
export { registry as gateRegistry };
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue