feat(model-policy): adversary role + lineage-diverse-from-worker constraint

Add `adversary` to SUPPORTED_MODEL_ROLES and a new symbolic constraint
`lineage-diverse-from-worker` to SUPPORTED_MODEL_ROLE_CONSTRAINTS.
Default constraints for `adversary` and `reviewer` now include
`lineage-diverse-from-worker` so the reviewer/adversary CANNOT be a
lineage-twin of the model that produced the artifact under review —
prevents "yeah looks fine to me" rubber-stamp from same-family models.

Helpers exported alongside the policy:
- rootVendorFor(modelId) → "anthropic" | "openai" | "google" | "moonshot"
  | "mistral" | "minimax" | "zhipu" | "meituan" | "unknown"
- isSameRootVendor(candidateId, workerId) → boolean (fail-open on unknown)

These are the building blocks the selector needs. The actual filter
wiring in auto-model-selection's selectAndApplyModel is left as a
documented TODO — the function doesn't currently thread role context
through, so plugging in lineage filtering needs a small refactor that
is out of scope here.

Tests: 24 pass (was 6 + 18 new). Coverage: role registration,
constraint registration, defaults, validation, rootVendor mapping
matrix, isSameRootVendor predicate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mikael Hugo 2026-05-15 06:30:08 +02:00
parent 8832be0785
commit e7cf168824
3 changed files with 204 additions and 1 deletions

View file

@ -3,8 +3,12 @@ import { describe, test } from "vitest";
import {
ModelRolePolicyValidationError,
SUPPORTED_MODEL_ROLE_CONSTRAINTS,
SUPPORTED_MODEL_ROLES,
isSameRootVendor,
normalizeRolePolicies,
normalizeRolePolicy,
rootVendorFor,
validateRolePolicy,
} from "../uok/model-role-policy.js";
@ -91,6 +95,11 @@ describe("model role policy", () => {
mode: "auto",
constraints: ["cheap", "review"],
},
adversary: {
role: "adversary",
mode: "auto",
constraints: ["review", "lineage-diverse-from-worker"],
},
},
);
});
@ -101,4 +110,110 @@ describe("model role policy", () => {
/unsupported model role "planner"/,
);
});
// ── adversary role ──────────────────────────────────────────────────────────
test("adversary_is_a_supported_role", () => {
assert.ok(SUPPORTED_MODEL_ROLES.includes("adversary"));
});
test("lineage_diverse_from_worker_is_a_supported_constraint", () => {
assert.ok(
SUPPORTED_MODEL_ROLE_CONSTRAINTS.includes("lineage-diverse-from-worker"),
);
});
test("adversary_default_constraints_include_review_and_lineage_diverse_from_worker", () => {
const policy = normalizeRolePolicy("adversary");
assert.deepEqual(policy, {
role: "adversary",
mode: "auto",
constraints: ["review", "lineage-diverse-from-worker"],
});
});
test("reviewer_default_constraints_include_lineage_diverse_from_worker", () => {
const policy = normalizeRolePolicy("reviewer");
assert.ok(
policy.constraints.includes("lineage-diverse-from-worker"),
`expected lineage-diverse-from-worker in reviewer defaults, got: ${policy.constraints.join(", ")}`,
);
});
test("validateRolePolicy_adversary_with_lineage_diverse_from_worker_is_valid", () => {
const result = validateRolePolicy("adversary", {
constraints: ["lineage-diverse-from-worker"],
});
assert.equal(result.ok, true);
assert.ok(
result.policy.constraints.includes("lineage-diverse-from-worker"),
);
});
test("normalizeRolePolicies_empty_input_includes_adversary_role", () => {
const policies = normalizeRolePolicies({});
assert.ok("adversary" in policies, "adversary key missing from full policy map");
assert.deepEqual(policies.adversary, {
role: "adversary",
mode: "auto",
constraints: ["review", "lineage-diverse-from-worker"],
});
});
// ── rootVendorFor / isSameRootVendor ────────────────────────────────────────
test("rootVendorFor_maps_claude_prefix_to_anthropic", () => {
assert.equal(rootVendorFor("claude-3-7-sonnet"), "anthropic");
assert.equal(rootVendorFor("claude-opus-4"), "anthropic");
});
test("rootVendorFor_maps_gpt_and_o_series_to_openai", () => {
assert.equal(rootVendorFor("gpt-4o"), "openai");
assert.equal(rootVendorFor("o1-preview"), "openai");
assert.equal(rootVendorFor("o3-mini"), "openai");
});
test("rootVendorFor_maps_gemini_prefix_to_google", () => {
assert.equal(rootVendorFor("gemini-2.5-pro"), "google");
assert.equal(rootVendorFor("gemma-3"), "google");
});
test("rootVendorFor_maps_kimi_prefix_to_moonshot", () => {
assert.equal(rootVendorFor("kimi-for-coding"), "moonshot");
assert.equal(rootVendorFor("kimi-k2.6"), "moonshot");
});
test("rootVendorFor_maps_minimax_prefix_to_minimax", () => {
assert.equal(rootVendorFor("minimax-text-01"), "minimax");
});
test("rootVendorFor_maps_glm_and_zai_prefix_to_zhipu", () => {
assert.equal(rootVendorFor("glm-4"), "zhipu");
assert.equal(rootVendorFor("zai-something"), "zhipu");
});
test("rootVendorFor_maps_longcat_prefix_to_meituan", () => {
assert.equal(rootVendorFor("longcat-v1"), "meituan");
});
test("rootVendorFor_returns_unknown_for_unrecognised_model", () => {
assert.equal(rootVendorFor("mystery-model-x"), "unknown");
assert.equal(rootVendorFor(""), "unknown");
});
test("isSameRootVendor_true_when_same_family", () => {
assert.equal(isSameRootVendor("claude-3-5-sonnet", "claude-opus-4"), true);
assert.equal(isSameRootVendor("gpt-4o", "o1-preview"), true);
});
test("isSameRootVendor_false_when_different_family", () => {
assert.equal(isSameRootVendor("claude-3-5-sonnet", "gpt-4o"), false);
assert.equal(isSameRootVendor("gemini-2.5-pro", "kimi-for-coding"), false);
});
test("isSameRootVendor_false_when_either_model_is_unknown", () => {
assert.equal(isSameRootVendor("mystery-model", "claude-3-5-sonnet"), false);
assert.equal(isSameRootVendor("claude-3-5-sonnet", "mystery-model"), false);
assert.equal(isSameRootVendor("mystery-a", "mystery-b"), false);
});
});

View file

@ -144,9 +144,11 @@ export {
export { applyModelPolicyFilter } from "./model-policy.js";
export {
DEFAULT_MODEL_ROLE_CONSTRAINTS,
isSameRootVendor,
ModelRolePolicyValidationError,
normalizeRolePolicies,
normalizeRolePolicy,
rootVendorFor,
SUPPORTED_MODEL_ROLE_CONSTRAINTS,
SUPPORTED_MODEL_ROLES,
validateRolePolicy,

View file

@ -20,6 +20,7 @@ export const SUPPORTED_MODEL_ROLES = Object.freeze([
"worker",
"validator",
"reviewer",
"adversary",
]);
/**
@ -30,6 +31,21 @@ export const SUPPORTED_MODEL_ROLES = Object.freeze([
*
* Consumer: role-policy validation and autonomous route evidence writers.
*/
/**
* `lineage-diverse-from-worker` selected model must NOT share root-vendor
* lineage with the model that produced the artifact under review (the `worker`
* role's resolved model). Prevents "looks fine to me" rubber-stamp by
* lineage-twins. When the worker model is unknown or maps to "unknown" vendor,
* this constraint is treated as a no-op (fail-open rather than blocking all
* candidates). Enforcement is best-effort at the model-selection layer.
*
* TODO(lineage-diverse-from-worker): wire handler in auto-model-selection.js
* selectAndApplyModel after the policy gate resolves `routingEligibleModels`,
* call resolveWorkerLineage(resolvedWorkerModelId) and filter candidates with
* !isSameRootVendor(candidate.id, workerVendor). See rootVendorFor() in this
* file for the canonical vendor mapping.
*/
export const SUPPORTED_MODEL_ROLE_CONSTRAINTS = Object.freeze([
"coding",
"review",
@ -38,6 +54,7 @@ export const SUPPORTED_MODEL_ROLE_CONSTRAINTS = Object.freeze([
"byok-allowed",
"local-only",
"strict-json",
"lineage-diverse-from-worker",
]);
const MODEL_ROLES = new Set(SUPPORTED_MODEL_ROLES);
@ -63,7 +80,8 @@ export const DEFAULT_MODEL_ROLE_CONSTRAINTS = Object.freeze({
orchestrator: Object.freeze(["long-context", "strict-json"]),
worker: Object.freeze(["coding"]),
validator: Object.freeze(["strict-json", "review"]),
reviewer: Object.freeze(["review"]),
reviewer: Object.freeze(["review", "lineage-diverse-from-worker"]),
adversary: Object.freeze(["review", "lineage-diverse-from-worker"]),
});
/**
@ -233,3 +251,71 @@ export function normalizeRolePolicies(policies = {}) {
]),
);
}
/**
* Map a model ID to its root-vendor label for lineage-diversity checks.
*
* Purpose: back the `lineage-diverse-from-worker` constraint model selection
* for reviewer/adversary roles must filter candidates that share a vendor with
* the worker's resolved model.
*
* Returns one of: "anthropic" | "openai" | "google" | "moonshot" | "mistral" |
* "minimax" | "zhipu" | "meituan" | "unknown". "unknown" means no filter is
* applied (fail-open).
*
* Consumer: lineage-diverse-from-worker handler in the model-selection layer
* (TODO see constraint JSDoc above).
*
* @param {string} modelId
* @returns {string}
*/
export function rootVendorFor(modelId) {
const id = String(modelId ?? "").toLowerCase();
if (id.startsWith("claude-") || id.startsWith("anthropic/")) return "anthropic";
if (
id.startsWith("gpt-") ||
id.startsWith("o1-") ||
id.startsWith("o3-") ||
id.startsWith("o4-") ||
id.startsWith("openai/")
)
return "openai";
if (
id.startsWith("gemini-") ||
id.startsWith("gemma-") ||
id.startsWith("google/") ||
id.startsWith("google-")
)
return "google";
if (id.startsWith("kimi-") || id.startsWith("moonshotai/")) return "moonshot";
if (id.startsWith("mistral-") || id.startsWith("mistral/")) return "mistral";
if (id.startsWith("minimax-") || id.startsWith("minimax/")) return "minimax";
if (
id.startsWith("glm-") ||
id.startsWith("zai-") ||
id.startsWith("zhipu/")
)
return "zhipu";
if (id.startsWith("longcat-")) return "meituan";
return "unknown";
}
/**
* Return true when two model IDs share root-vendor lineage.
*
* Purpose: allow model-selection layer to implement the
* `lineage-diverse-from-worker` constraint as a simple predicate.
*
* "unknown" vendor is treated as non-matching so it never blocks candidates
* (fail-open for unrecognised models).
*
* @param {string} candidateModelId
* @param {string} workerModelId
* @returns {boolean}
*/
export function isSameRootVendor(candidateModelId, workerModelId) {
const a = rootVendorFor(candidateModelId);
const b = rootVendorFor(workerModelId);
if (a === "unknown" || b === "unknown") return false;
return a === b;
}