From 1ecb0b9df8edc4d697505be26cbfa45e525eb04e Mon Sep 17 00:00:00 2001 From: Luis Manrique Date: Wed, 5 Aug 2026 04:22:55 +0000 Subject: [PATCH 1/4] feat: add fail-closed preference pair admission --- src/index.ts | 9 ++ src/seed43-preference-admission.ts | 138 +++++++++++++++++++++ tests/seed43-preference-admission.test.mjs | 74 +++++++++++ 3 files changed, 221 insertions(+) create mode 100644 src/seed43-preference-admission.ts create mode 100644 tests/seed43-preference-admission.test.mjs diff --git a/src/index.ts b/src/index.ts index f75eaaa7..b502ffbe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -63,6 +63,15 @@ import { daemonStatus } from "./internal/daemon.js"; import { readCliVersion, readManifestVersions } from "./internal/version.js"; import { installedPackageRoot } from "./internal/package-root.js"; +export { + SEED43_PREFERENCE_REJECTION_CLASSES, + admitSeed43PreferencePairs, + sha256TokenSequence as sha256PreferenceTokenSequence, + type PreferenceToolCall, + type Seed43PreferenceAdmissionResult, + type Seed43PreferencePair, +} from "./seed43-preference-admission.js"; + export { compileTraceFoundry, createTraceReplayPlan, importTraceReviews, runTraceReplays } from "./trace-foundry.js"; export { serveTraceFoundry } from "./trace-foundry-server.js"; export { buildRejectionGuidance, classifyRejection, computeRecoveryOverJournals, computeRecoveryRates, loadGuidanceFile, readRolloutJournals, synthesizeMinimalExample } from "./rejection-guidance.js"; diff --git a/src/seed43-preference-admission.ts b/src/seed43-preference-admission.ts new file mode 100644 index 00000000..5e763c80 --- /dev/null +++ b/src/seed43-preference-admission.ts @@ -0,0 +1,138 @@ +import { createHash } from "node:crypto"; + +export type PreferenceToolCall = { + name: string; + arguments: unknown; + effects: readonly string[]; +}; + +export type Seed43PreferencePair = { + pairId: string; + rowId: string; + promptHistorySha256: string; + actionRequired: boolean; + chosenTokens: readonly number[]; + rejectedTokens: readonly number[]; + chosenToolCalls: readonly PreferenceToolCall[]; + expectedToolCalls: readonly PreferenceToolCall[]; + rejectionClass: string; + sourceCapabilitySha256: string; + sourceRowIdSha256: string; + rendererSha256: string; + toolSchemaSha256: string; + runtimeSha256: string; + rejectedPayloadAvailable: boolean; +}; + +export type Seed43PreferenceAdmissionResult = { + admitted: boolean; + pairCount: number; + actionPairCount: number; + noActionPairCount: number; + rejectionClusters: Record; + rejectedPairIds: string[]; +}; + +export const SEED43_PREFERENCE_REJECTION_CLASSES = Object.freeze([ + "wrong_tool", + "wrong_arguments_or_effects", + "missing_continuation", + "false_positive_mutation", + "collapse_sentinel", +]); + +function stableJson(value: unknown): string { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; + return `{${Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`) + .join(",")}}`; +} + +function sameToolCall(actual: PreferenceToolCall, expected: PreferenceToolCall): boolean { + return actual.name === expected.name + && stableJson(actual.arguments) === stableJson(expected.arguments) + && stableJson(actual.effects) === stableJson(expected.effects); +} + +function hasToolContract(pair: Seed43PreferencePair): boolean { + return pair.chosenToolCalls.length > 0 + && pair.chosenToolCalls.length === pair.expectedToolCalls.length + && pair.chosenToolCalls.every((call, index) => sameToolCall(call, pair.expectedToolCalls[index])); +} + +function reject( + clusters: Record, + rejectedPairIds: string[], + pairId: string, + cluster: string, +): void { + clusters[cluster] = (clusters[cluster] ?? 0) + 1; + rejectedPairIds.push(pairId); +} + +export function sha256TokenSequence(tokens: readonly number[]): string { + return createHash("sha256").update(JSON.stringify(tokens)).digest("hex"); +} + +export function admitSeed43PreferencePairs( + pairs: readonly Seed43PreferencePair[], + protectedRejectionClasses: readonly string[] = SEED43_PREFERENCE_REJECTION_CLASSES, +): Seed43PreferenceAdmissionResult { + const rejectionClusters: Record = {}; + const rejectedPairIds: string[] = []; + let actionPairCount = 0; + let noActionPairCount = 0; + const observedRejectionClasses = new Set(); + + for (const pair of pairs) { + if (pair.actionRequired) actionPairCount += 1; + else noActionPairCount += 1; + observedRejectionClasses.add(pair.rejectionClass); + + if (pair.chosenTokens.length === 0 || pair.rejectedTokens.length === 0) { + reject(rejectionClusters, rejectedPairIds, pair.pairId, "empty_preference_side"); + } + if (sha256TokenSequence(pair.chosenTokens) === sha256TokenSequence(pair.rejectedTokens)) { + reject(rejectionClusters, rejectedPairIds, pair.pairId, "identical_chosen_rejected"); + } + if (pair.actionRequired ? !hasToolContract(pair) : pair.chosenToolCalls.length > 0) { + reject(rejectionClusters, rejectedPairIds, pair.pairId, "chosen_contract_mismatch"); + } + if (pair.chosenTokens.length === 3 + && pair.chosenTokens[0] === 16071 + && pair.chosenTokens[1] === 95597 + && pair.chosenTokens[2] === 11) { + reject(rejectionClusters, rejectedPairIds, pair.pairId, "chosen_collapse_sentinel"); + } + if (!SEED43_PREFERENCE_REJECTION_CLASSES.includes(pair.rejectionClass)) { + reject(rejectionClusters, rejectedPairIds, pair.pairId, "unknown_rejection_class"); + } + if (!pair.rejectedPayloadAvailable) { + reject(rejectionClusters, rejectedPairIds, pair.pairId, "missing_rejected_payload"); + } + if (pair.promptHistorySha256.length !== 64 + || pair.sourceCapabilitySha256.length !== 64 + || pair.sourceRowIdSha256.length !== 64 + || pair.rendererSha256.length !== 64 + || pair.toolSchemaSha256.length !== 64 + || pair.runtimeSha256.length !== 64) { + reject(rejectionClusters, rejectedPairIds, pair.pairId, "missing_provenance_hash"); + } + } + + if (pairs.length === 0) rejectionClusters.missing_pairs = 1; + if (noActionPairCount > actionPairCount) rejectionClusters.no_action_dominates = 1; + const missingClasses = protectedRejectionClasses.filter((item) => !observedRejectionClasses.has(item)); + if (missingClasses.length > 0) rejectionClusters.missing_rejection_class = missingClasses.length; + + return { + admitted: Object.keys(rejectionClusters).length === 0, + pairCount: pairs.length, + actionPairCount, + noActionPairCount, + rejectionClusters, + rejectedPairIds, + }; +} diff --git a/tests/seed43-preference-admission.test.mjs b/tests/seed43-preference-admission.test.mjs new file mode 100644 index 00000000..bf947cf6 --- /dev/null +++ b/tests/seed43-preference-admission.test.mjs @@ -0,0 +1,74 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { + SEED43_PREFERENCE_REJECTION_CLASSES, + admitSeed43PreferencePairs, +} from "../dist/seed43-preference-admission.js"; + +const hash = "a".repeat(64); + +function pair(overrides = {}) { + return { + pairId: "pair-1", + rowId: "row-1", + promptHistorySha256: hash, + actionRequired: true, + chosenTokens: [1, 2, 3], + rejectedTokens: [4, 5, 6], + chosenToolCalls: [{ name: "write", arguments: { id: "x" }, effects: ["updated:x"] }], + expectedToolCalls: [{ name: "write", arguments: { id: "x" }, effects: ["updated:x"] }], + rejectionClass: "wrong_tool", + sourceCapabilitySha256: hash, + sourceRowIdSha256: hash, + rendererSha256: hash, + toolSchemaSha256: hash, + runtimeSha256: hash, + rejectedPayloadAvailable: true, + ...overrides, + }; +} + +describe("seed43 preference admission", () => { + it("admits balanced, hash-bound pairs with exact chosen contracts", () => { + const result = admitSeed43PreferencePairs([ + pair(), + pair({ + pairId: "pair-2", + rowId: "row-2", + actionRequired: false, + chosenToolCalls: [], + expectedToolCalls: [], + rejectionClass: "false_positive_mutation", + }), + ], ["wrong_tool", "false_positive_mutation"]); + assert.equal(result.admitted, true); + assert.equal(result.actionPairCount, 1); + assert.equal(result.noActionPairCount, 1); + }); + + it("rejects missing rejected payloads, chosen mismatches, and imbalance", () => { + const result = admitSeed43PreferencePairs([ + pair({ + rejectedPayloadAvailable: false, + chosenToolCalls: [{ name: "write", arguments: { id: "wrong" }, effects: [] }], + }), + pair({ pairId: "pair-2", actionRequired: false, chosenToolCalls: [], expectedToolCalls: [] }), + pair({ pairId: "pair-3", actionRequired: false, chosenToolCalls: [], expectedToolCalls: [] }), + ]); + assert.equal(result.admitted, false); + assert.equal(result.rejectionClusters.missing_rejected_payload, 1); + assert.equal(result.rejectionClusters.chosen_contract_mismatch, 1); + assert.equal(result.rejectionClusters.no_action_dominates, 1); + }); + + it("recognizes the protected rejection classes", () => { + assert.deepEqual([...SEED43_PREFERENCE_REJECTION_CLASSES], [ + "wrong_tool", + "wrong_arguments_or_effects", + "missing_continuation", + "false_positive_mutation", + "collapse_sentinel", + ]); + }); +}); From 8971ff3625a7772089cd2e13d7f2c4477cea2428 Mon Sep 17 00:00:00 2001 From: Luis Manrique Date: Wed, 5 Aug 2026 04:32:03 +0000 Subject: [PATCH 2/4] fix: harden preference admission provenance --- src/index.ts | 15 +- src/preference-admission.ts | 270 +++++++++++++++++++++ src/seed43-preference-admission.ts | 138 ----------- tests/preference-admission.test.mjs | 188 ++++++++++++++ tests/seed43-preference-admission.test.mjs | 74 ------ 5 files changed, 468 insertions(+), 217 deletions(-) create mode 100644 src/preference-admission.ts delete mode 100644 src/seed43-preference-admission.ts create mode 100644 tests/preference-admission.test.mjs delete mode 100644 tests/seed43-preference-admission.test.mjs diff --git a/src/index.ts b/src/index.ts index b502ffbe..b9984b72 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,13 +64,18 @@ import { readCliVersion, readManifestVersions } from "./internal/version.js"; import { installedPackageRoot } from "./internal/package-root.js"; export { - SEED43_PREFERENCE_REJECTION_CLASSES, - admitSeed43PreferencePairs, + PREFERENCE_REJECTION_CLASSES, + admitPreferencePairs, + isSha256, sha256TokenSequence as sha256PreferenceTokenSequence, + type PreferenceAdmissionConfig, + type PreferenceAdmissionRejection, + type PreferenceAdmissionResult, + type PreferencePair, + type PreferenceSemantic, type PreferenceToolCall, - type Seed43PreferenceAdmissionResult, - type Seed43PreferencePair, -} from "./seed43-preference-admission.js"; + type VerifiedRejectedPayload, +} from "./preference-admission.js"; export { compileTraceFoundry, createTraceReplayPlan, importTraceReviews, runTraceReplays } from "./trace-foundry.js"; export { serveTraceFoundry } from "./trace-foundry-server.js"; diff --git a/src/preference-admission.ts b/src/preference-admission.ts new file mode 100644 index 00000000..0a456f91 --- /dev/null +++ b/src/preference-admission.ts @@ -0,0 +1,270 @@ +import { createHash } from "node:crypto"; + +export type PreferenceToolCall = { + name: string; + arguments: unknown; + effects: readonly string[]; +}; + +export type PreferenceSemantic = { + kind: "tool_calls" | "no_action"; + toolCalls: readonly PreferenceToolCall[]; +}; + +export type PreferencePair = { + pairId: string; + rowId: string; + split: "train" | "dev" | "holdout"; + promptHistorySha256: string; + actionRequired: boolean; + actionFamily?: string; + multiEffectContinuation: boolean; + chosenTokens: readonly number[]; + chosenToolCalls: readonly PreferenceToolCall[]; + expectedToolCalls: readonly PreferenceToolCall[]; + expectedRejectionClass: string; + rejectedPayloadSha256: string; + sourceCapabilitySha256: string; + allowedTrainRowIdsSha256: string; + sourceRowIdSha256: string; + rendererSha256: string; + toolSchemaSha256: string; + runtimeSha256: string; +}; + +export type VerifiedRejectedPayload = { + payloadSha256: string; + classification: string; + semantic: PreferenceSemantic; +}; + +export type PreferenceAdmissionConfig = { + expectedSourceCapabilitySha256: string; + expectedAllowedTrainRowIdsSha256: string; + allowedTrainRowIds: ReadonlySet; + expectedRendererSha256: string; + expectedToolSchemaSha256: string; + expectedRuntimeSha256: string; + requiredRejectionClasses: readonly string[]; + desiredRejectionClasses?: readonly string[]; + requiredActionFamilies?: readonly string[]; + requireMultiEffectContinuation?: boolean; + allowMultipleRejectionsPerRow?: boolean; + verifyRejectedPayload: (pair: PreferencePair) => VerifiedRejectedPayload | null; +}; + +export type PreferenceAdmissionRejection = { + pairId: string | null; + rowId?: string; + reason: string; +}; + +export type PreferenceAdmissionResult = { + admitted: boolean; + pairCount: number; + actionPairCount: number; + noActionPairCount: number; + rejectionClusters: Record; + rejectedPairIds: string[]; + rejections: PreferenceAdmissionRejection[]; + globalRejections: PreferenceAdmissionRejection[]; + missingDesiredRejectionClasses: string[]; +}; + +export const PREFERENCE_REJECTION_CLASSES = Object.freeze([ + "wrong_tool", + "wrong_arguments_or_effects", + "missing_continuation", + "false_positive_mutation", + "collapse_sentinel", +]); + +const SHA256_PATTERN = /^[0-9a-f]{64}$/; + +function stableJson(value: unknown): string { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; + return `{${Object.entries(value as Record) + .sort(([left], [right]) => left.localeCompare(right)) + .map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`) + .join(",")}}`; +} + +function semanticForChosen(pair: PreferencePair): PreferenceSemantic { + return { + kind: pair.actionRequired ? "tool_calls" : "no_action", + toolCalls: pair.chosenToolCalls, + }; +} + +function sameToolCall(actual: PreferenceToolCall, expected: PreferenceToolCall): boolean { + return actual.name === expected.name + && stableJson(actual.arguments) === stableJson(expected.arguments) + && stableJson(actual.effects) === stableJson(expected.effects); +} + +function sameSemantic(left: PreferenceSemantic, right: PreferenceSemantic): boolean { + return left.kind === right.kind + && left.toolCalls.length === right.toolCalls.length + && left.toolCalls.every((call, index) => sameToolCall(call, right.toolCalls[index])); +} + +function hasToolContract(pair: PreferencePair): boolean { + return pair.actionRequired + ? pair.chosenToolCalls.length > 0 + && pair.chosenToolCalls.length === pair.expectedToolCalls.length + && pair.chosenToolCalls.every((call, index) => sameToolCall(call, pair.expectedToolCalls[index])) + : pair.chosenToolCalls.length === 0 && pair.expectedToolCalls.length === 0; +} + +function addRejection( + result: { + rejectionClusters: Record; + rejectedPairIds: string[]; + rejections: PreferenceAdmissionRejection[]; + }, + pair: PreferencePair, + reason: string, +): void { + result.rejectionClusters[reason] = (result.rejectionClusters[reason] ?? 0) + 1; + result.rejectedPairIds.push(pair.pairId); + result.rejections.push({ pairId: pair.pairId, rowId: pair.rowId, reason }); +} + +function addGlobalRejection( + result: { + rejectionClusters: Record; + globalRejections: PreferenceAdmissionRejection[]; + }, + reason: string, + pairIds: readonly string[], +): void { + result.rejectionClusters[reason] = (result.rejectionClusters[reason] ?? 0) + 1; + result.globalRejections.push({ pairId: null, rowId: pairIds.join(","), reason }); +} + +export function sha256TokenSequence(tokens: readonly number[]): string { + return createHash("sha256").update(JSON.stringify(tokens)).digest("hex"); +} + +export function isSha256(value: string): boolean { + return SHA256_PATTERN.test(value); +} + +export function admitPreferencePairs( + pairs: readonly PreferencePair[], + config: PreferenceAdmissionConfig, +): PreferenceAdmissionResult { + const result = { + rejectionClusters: {} as Record, + rejectedPairIds: [] as string[], + rejections: [] as PreferenceAdmissionRejection[], + globalRejections: [] as PreferenceAdmissionRejection[], + }; + let actionPairCount = 0; + let noActionPairCount = 0; + const rowIds = new Map(); + const pairIds = new Set(); + const observedClasses = new Set(); + const observedFamilies = new Set(); + const globalPairIds = pairs.map((pair) => pair.pairId); + + for (const pair of pairs) { + if (pair.actionRequired) actionPairCount += 1; + else noActionPairCount += 1; + rowIds.set(pair.rowId, [...(rowIds.get(pair.rowId) ?? []), pair.pairId]); + + if (pairIds.has(pair.pairId)) addRejection(result, pair, "duplicate_pair_id"); + pairIds.add(pair.pairId); + if (!config.allowMultipleRejectionsPerRow && rowIds.get(pair.rowId)?.length === 2) { + addRejection(result, pair, "duplicate_source_row_id"); + } + if (pair.split !== "train" || !config.allowedTrainRowIds.has(pair.rowId)) { + addRejection(result, pair, "row_outside_frozen_train_allowlist"); + } + if (pair.sourceCapabilitySha256 !== config.expectedSourceCapabilitySha256) { + addRejection(result, pair, "source_capability_mismatch"); + } + if (pair.allowedTrainRowIdsSha256 !== config.expectedAllowedTrainRowIdsSha256) { + addRejection(result, pair, "allowlist_hash_mismatch"); + } + if (pair.rendererSha256 !== config.expectedRendererSha256) addRejection(result, pair, "renderer_mismatch"); + if (pair.toolSchemaSha256 !== config.expectedToolSchemaSha256) addRejection(result, pair, "tool_schema_mismatch"); + if (pair.runtimeSha256 !== config.expectedRuntimeSha256) addRejection(result, pair, "runtime_mismatch"); + + const hashes = [ + pair.promptHistorySha256, + pair.rejectedPayloadSha256, + pair.sourceCapabilitySha256, + pair.allowedTrainRowIdsSha256, + pair.sourceRowIdSha256, + pair.rendererSha256, + pair.toolSchemaSha256, + pair.runtimeSha256, + ]; + if (hashes.some((hash) => !isSha256(hash))) addRejection(result, pair, "invalid_sha256"); + if (pair.actionRequired && pair.actionFamily) observedFamilies.add(pair.actionFamily); + if (!hasToolContract(pair)) addRejection(result, pair, "chosen_contract_mismatch"); + if (pair.actionRequired + && pair.chosenTokens.length === 3 + && pair.chosenTokens[0] === 16071 + && pair.chosenTokens[1] === 95597 + && pair.chosenTokens[2] === 11) { + addRejection(result, pair, "chosen_collapse_sentinel"); + } + if (pair.actionRequired + && pair.chosenTokens.length === 3 + && pair.chosenTokens[0] === 16071 + && pair.chosenTokens[1] === 95597 + && pair.chosenTokens[2] === 11) { + addRejection(result, pair, "chosen_collapse_sentinel"); + } + + const verified = config.verifyRejectedPayload(pair); + if (verified === null) { + addRejection(result, pair, "missing_rejected_payload_preimage"); + continue; + } + if (!isSha256(verified.payloadSha256) || verified.payloadSha256 !== pair.rejectedPayloadSha256) { + addRejection(result, pair, "rejected_payload_hash_mismatch"); + } + if (verified.classification !== pair.expectedRejectionClass) { + addRejection(result, pair, "rejection_class_mismatch"); + } + if (!PREFERENCE_REJECTION_CLASSES.includes(verified.classification)) { + addRejection(result, pair, "unknown_rejection_class"); + } + observedClasses.add(verified.classification); + if (sameSemantic(semanticForChosen(pair), verified.semantic)) { + addRejection(result, pair, "identical_chosen_rejected_semantics"); + } + } + + if (pairs.length === 0) addGlobalRejection(result, "missing_pairs", globalPairIds); + if (noActionPairCount > actionPairCount) addGlobalRejection(result, "no_action_dominates", globalPairIds); + if (actionPairCount === 0) addGlobalRejection(result, "missing_action_pairs", globalPairIds); + for (const requiredFamily of config.requiredActionFamilies ?? []) { + if (!observedFamilies.has(requiredFamily)) { + addGlobalRejection(result, `missing_action_family:${requiredFamily}`, globalPairIds); + } + } + if (config.requireMultiEffectContinuation && !pairs.some((pair) => pair.actionRequired && pair.multiEffectContinuation)) { + addGlobalRejection(result, "missing_multi_effect_continuation", globalPairIds); + } + for (const requiredClass of config.requiredRejectionClasses) { + if (!observedClasses.has(requiredClass)) { + addGlobalRejection(result, `missing_rejection_class:${requiredClass}`, globalPairIds); + } + } + const missingDesiredRejectionClasses = (config.desiredRejectionClasses ?? []) + .filter((desiredClass) => !observedClasses.has(desiredClass)); + + return { + admitted: result.rejections.length === 0 && result.globalRejections.length === 0, + pairCount: pairs.length, + actionPairCount, + noActionPairCount, + ...result, + missingDesiredRejectionClasses, + }; +} diff --git a/src/seed43-preference-admission.ts b/src/seed43-preference-admission.ts deleted file mode 100644 index 5e763c80..00000000 --- a/src/seed43-preference-admission.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { createHash } from "node:crypto"; - -export type PreferenceToolCall = { - name: string; - arguments: unknown; - effects: readonly string[]; -}; - -export type Seed43PreferencePair = { - pairId: string; - rowId: string; - promptHistorySha256: string; - actionRequired: boolean; - chosenTokens: readonly number[]; - rejectedTokens: readonly number[]; - chosenToolCalls: readonly PreferenceToolCall[]; - expectedToolCalls: readonly PreferenceToolCall[]; - rejectionClass: string; - sourceCapabilitySha256: string; - sourceRowIdSha256: string; - rendererSha256: string; - toolSchemaSha256: string; - runtimeSha256: string; - rejectedPayloadAvailable: boolean; -}; - -export type Seed43PreferenceAdmissionResult = { - admitted: boolean; - pairCount: number; - actionPairCount: number; - noActionPairCount: number; - rejectionClusters: Record; - rejectedPairIds: string[]; -}; - -export const SEED43_PREFERENCE_REJECTION_CLASSES = Object.freeze([ - "wrong_tool", - "wrong_arguments_or_effects", - "missing_continuation", - "false_positive_mutation", - "collapse_sentinel", -]); - -function stableJson(value: unknown): string { - if (value === null || typeof value !== "object") return JSON.stringify(value); - if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`; - return `{${Object.entries(value as Record) - .sort(([left], [right]) => left.localeCompare(right)) - .map(([key, item]) => `${JSON.stringify(key)}:${stableJson(item)}`) - .join(",")}}`; -} - -function sameToolCall(actual: PreferenceToolCall, expected: PreferenceToolCall): boolean { - return actual.name === expected.name - && stableJson(actual.arguments) === stableJson(expected.arguments) - && stableJson(actual.effects) === stableJson(expected.effects); -} - -function hasToolContract(pair: Seed43PreferencePair): boolean { - return pair.chosenToolCalls.length > 0 - && pair.chosenToolCalls.length === pair.expectedToolCalls.length - && pair.chosenToolCalls.every((call, index) => sameToolCall(call, pair.expectedToolCalls[index])); -} - -function reject( - clusters: Record, - rejectedPairIds: string[], - pairId: string, - cluster: string, -): void { - clusters[cluster] = (clusters[cluster] ?? 0) + 1; - rejectedPairIds.push(pairId); -} - -export function sha256TokenSequence(tokens: readonly number[]): string { - return createHash("sha256").update(JSON.stringify(tokens)).digest("hex"); -} - -export function admitSeed43PreferencePairs( - pairs: readonly Seed43PreferencePair[], - protectedRejectionClasses: readonly string[] = SEED43_PREFERENCE_REJECTION_CLASSES, -): Seed43PreferenceAdmissionResult { - const rejectionClusters: Record = {}; - const rejectedPairIds: string[] = []; - let actionPairCount = 0; - let noActionPairCount = 0; - const observedRejectionClasses = new Set(); - - for (const pair of pairs) { - if (pair.actionRequired) actionPairCount += 1; - else noActionPairCount += 1; - observedRejectionClasses.add(pair.rejectionClass); - - if (pair.chosenTokens.length === 0 || pair.rejectedTokens.length === 0) { - reject(rejectionClusters, rejectedPairIds, pair.pairId, "empty_preference_side"); - } - if (sha256TokenSequence(pair.chosenTokens) === sha256TokenSequence(pair.rejectedTokens)) { - reject(rejectionClusters, rejectedPairIds, pair.pairId, "identical_chosen_rejected"); - } - if (pair.actionRequired ? !hasToolContract(pair) : pair.chosenToolCalls.length > 0) { - reject(rejectionClusters, rejectedPairIds, pair.pairId, "chosen_contract_mismatch"); - } - if (pair.chosenTokens.length === 3 - && pair.chosenTokens[0] === 16071 - && pair.chosenTokens[1] === 95597 - && pair.chosenTokens[2] === 11) { - reject(rejectionClusters, rejectedPairIds, pair.pairId, "chosen_collapse_sentinel"); - } - if (!SEED43_PREFERENCE_REJECTION_CLASSES.includes(pair.rejectionClass)) { - reject(rejectionClusters, rejectedPairIds, pair.pairId, "unknown_rejection_class"); - } - if (!pair.rejectedPayloadAvailable) { - reject(rejectionClusters, rejectedPairIds, pair.pairId, "missing_rejected_payload"); - } - if (pair.promptHistorySha256.length !== 64 - || pair.sourceCapabilitySha256.length !== 64 - || pair.sourceRowIdSha256.length !== 64 - || pair.rendererSha256.length !== 64 - || pair.toolSchemaSha256.length !== 64 - || pair.runtimeSha256.length !== 64) { - reject(rejectionClusters, rejectedPairIds, pair.pairId, "missing_provenance_hash"); - } - } - - if (pairs.length === 0) rejectionClusters.missing_pairs = 1; - if (noActionPairCount > actionPairCount) rejectionClusters.no_action_dominates = 1; - const missingClasses = protectedRejectionClasses.filter((item) => !observedRejectionClasses.has(item)); - if (missingClasses.length > 0) rejectionClusters.missing_rejection_class = missingClasses.length; - - return { - admitted: Object.keys(rejectionClusters).length === 0, - pairCount: pairs.length, - actionPairCount, - noActionPairCount, - rejectionClusters, - rejectedPairIds, - }; -} diff --git a/tests/preference-admission.test.mjs b/tests/preference-admission.test.mjs new file mode 100644 index 00000000..8d41daf4 --- /dev/null +++ b/tests/preference-admission.test.mjs @@ -0,0 +1,188 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { + PREFERENCE_REJECTION_CLASSES, + admitPreferencePairs, +} from "../dist/preference-admission.js"; + +const hash = "a".repeat(64); +const allowlist = new Set(["row-1", "row-2"]); + +function pair(overrides = {}) { + return { + pairId: "pair-1", + rowId: "row-1", + split: "train", + promptHistorySha256: hash, + actionRequired: true, + actionFamily: "write", + multiEffectContinuation: false, + chosenTokens: [1, 2, 3], + chosenToolCalls: [{ name: "write", arguments: { id: "x" }, effects: ["updated:x"] }], + expectedToolCalls: [{ name: "write", arguments: { id: "x" }, effects: ["updated:x"] }], + expectedRejectionClass: "wrong_tool", + rejectedPayloadSha256: "b".repeat(64), + sourceCapabilitySha256: hash, + allowedTrainRowIdsSha256: hash, + sourceRowIdSha256: hash, + rendererSha256: hash, + toolSchemaSha256: hash, + runtimeSha256: hash, + ...overrides, + }; +} + +function config(verifyRejectedPayload, overrides = {}) { + return { + expectedSourceCapabilitySha256: hash, + expectedAllowedTrainRowIdsSha256: hash, + allowedTrainRowIds: allowlist, + expectedRendererSha256: hash, + expectedToolSchemaSha256: hash, + expectedRuntimeSha256: hash, + requiredRejectionClasses: ["wrong_tool", "false_positive_mutation"], + requiredActionFamilies: ["write"], + requireMultiEffectContinuation: true, + verifyRejectedPayload, + ...overrides, + }; +} + +function verified(overrides = {}) { + return { + payloadSha256: "b".repeat(64), + classification: "wrong_tool", + semantic: { kind: "no_action", toolCalls: [] }, + ...overrides, + }; +} + +describe("preference admission", () => { + it("admits verified, balanced, hash-bound pairs with family coverage", () => { + const result = admitPreferencePairs([ + pair({ multiEffectContinuation: true }), + pair({ + pairId: "pair-2", + rowId: "row-2", + actionRequired: false, + actionFamily: undefined, + chosenToolCalls: [], + expectedToolCalls: [], + expectedRejectionClass: "false_positive_mutation", + rejectedPayloadSha256: "c".repeat(64), + }), + ], config((candidate) => candidate.pairId === "pair-1" + ? verified() + : verified({ + payloadSha256: "c".repeat(64), + classification: "false_positive_mutation", + semantic: { kind: "tool_calls", toolCalls: [{ name: "write", arguments: { id: "x" }, effects: ["updated:x"] }] }, + }))); + assert.equal(result.admitted, true); + }); + + it("fails closed on lied availability and nonhex hashes", () => { + const result = admitPreferencePairs([ + pair({ rejectedPayloadSha256: "z".repeat(64) }), + ], config(() => null, { requireMultiEffectContinuation: false })); + assert.equal(result.admitted, false); + assert.match(result.rejections.map((item) => item.reason).join(","), /invalid_sha256/); + assert.match(result.rejections.map((item) => item.reason).join(","), /missing_rejected_payload_preimage/); + }); + + it("rejects duplicates, cross-split rows, mixed provenance, and semantic matches", () => { + const result = admitPreferencePairs([ + pair({ pairId: "duplicate", split: "dev", runtimeSha256: "d".repeat(64) }), + pair({ pairId: "duplicate", rowId: "row-2", sourceCapabilitySha256: "e".repeat(64) }), + ], config(() => verified({ + semantic: { kind: "tool_calls", toolCalls: [{ name: "write", arguments: { id: "x" }, effects: ["updated:x"] }] }, + }), { requireMultiEffectContinuation: false })); + assert.equal(result.admitted, false); + const reasons = result.rejections.map((item) => item.reason).join(","); + assert.match(reasons, /duplicate_pair_id/); + assert.match(reasons, /row_outside_frozen_train_allowlist/); + assert.match(reasons, /runtime_mismatch/); + assert.match(reasons, /source_capability_mismatch/); + assert.match(reasons, /identical_chosen_rejected_semantics/); + }); + + it("rejects caller-mislabeled classes and reports global coverage failures", () => { + const result = admitPreferencePairs([ + pair({ expectedRejectionClass: "false_positive_mutation" }), + ], config(() => verified(), { + requiredActionFamilies: ["read", "write"], + requireMultiEffectContinuation: true, + requiredRejectionClasses: [...PREFERENCE_REJECTION_CLASSES], + })); + assert.equal(result.admitted, false); + assert.match(result.rejections.map((item) => item.reason).join(","), /rejection_class_mismatch/); + const globals = result.globalRejections.map((item) => item.reason).join(","); + assert.match(globals, /missing_action_family:read/); + assert.match(globals, /missing_multi_effect_continuation/); + assert.match(globals, /missing_rejection_class:collapse_sentinel/); + }); + + it("accepts the no-action sentinel when the row contract is no-action", () => { + const result = admitPreferencePairs([ + pair({ + multiEffectContinuation: true, + expectedRejectionClass: "false_positive_mutation", + }), + pair({ + pairId: "pair-2", + rowId: "row-2", + actionRequired: false, + actionFamily: undefined, + chosenTokens: [16071, 95597, 11], + chosenToolCalls: [], + expectedToolCalls: [], + expectedRejectionClass: "false_positive_mutation", + }), + ], config((candidate) => candidate.pairId === "pair-2" + ? verified({ + classification: "false_positive_mutation", + semantic: { + kind: "tool_calls", + toolCalls: [{ name: "write", arguments: { id: "x" }, effects: ["updated:x"] }], + }, + }) + : verified({ + classification: "false_positive_mutation", + semantic: { kind: "no_action", toolCalls: [] }, + }), { + requireMultiEffectContinuation: false, + requiredRejectionClasses: ["false_positive_mutation"], + })); + assert.equal(result.admitted, true); + }); + + it("distinguishes identical tokens with opposite action signs by semantic outcome", () => { + const result = admitPreferencePairs([ + pair({ multiEffectContinuation: true }), + pair({ + pairId: "pair-2", + rowId: "row-2", + actionRequired: false, + actionFamily: undefined, + chosenTokens: [16071, 95597, 11], + chosenToolCalls: [], + expectedToolCalls: [], + expectedRejectionClass: "wrong_tool", + }), + ], config((candidate) => candidate.pairId === "pair-2" + ? verified({ + semantic: { + kind: "tool_calls", + toolCalls: [{ name: "write", arguments: { id: "x" }, effects: ["updated:x"] }], + }, + }) + : verified({ + semantic: { kind: "no_action", toolCalls: [] }, + }), { + requireMultiEffectContinuation: false, + requiredRejectionClasses: ["wrong_tool"], + })); + assert.equal(result.admitted, true); + }); +}); diff --git a/tests/seed43-preference-admission.test.mjs b/tests/seed43-preference-admission.test.mjs deleted file mode 100644 index bf947cf6..00000000 --- a/tests/seed43-preference-admission.test.mjs +++ /dev/null @@ -1,74 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; - -import { - SEED43_PREFERENCE_REJECTION_CLASSES, - admitSeed43PreferencePairs, -} from "../dist/seed43-preference-admission.js"; - -const hash = "a".repeat(64); - -function pair(overrides = {}) { - return { - pairId: "pair-1", - rowId: "row-1", - promptHistorySha256: hash, - actionRequired: true, - chosenTokens: [1, 2, 3], - rejectedTokens: [4, 5, 6], - chosenToolCalls: [{ name: "write", arguments: { id: "x" }, effects: ["updated:x"] }], - expectedToolCalls: [{ name: "write", arguments: { id: "x" }, effects: ["updated:x"] }], - rejectionClass: "wrong_tool", - sourceCapabilitySha256: hash, - sourceRowIdSha256: hash, - rendererSha256: hash, - toolSchemaSha256: hash, - runtimeSha256: hash, - rejectedPayloadAvailable: true, - ...overrides, - }; -} - -describe("seed43 preference admission", () => { - it("admits balanced, hash-bound pairs with exact chosen contracts", () => { - const result = admitSeed43PreferencePairs([ - pair(), - pair({ - pairId: "pair-2", - rowId: "row-2", - actionRequired: false, - chosenToolCalls: [], - expectedToolCalls: [], - rejectionClass: "false_positive_mutation", - }), - ], ["wrong_tool", "false_positive_mutation"]); - assert.equal(result.admitted, true); - assert.equal(result.actionPairCount, 1); - assert.equal(result.noActionPairCount, 1); - }); - - it("rejects missing rejected payloads, chosen mismatches, and imbalance", () => { - const result = admitSeed43PreferencePairs([ - pair({ - rejectedPayloadAvailable: false, - chosenToolCalls: [{ name: "write", arguments: { id: "wrong" }, effects: [] }], - }), - pair({ pairId: "pair-2", actionRequired: false, chosenToolCalls: [], expectedToolCalls: [] }), - pair({ pairId: "pair-3", actionRequired: false, chosenToolCalls: [], expectedToolCalls: [] }), - ]); - assert.equal(result.admitted, false); - assert.equal(result.rejectionClusters.missing_rejected_payload, 1); - assert.equal(result.rejectionClusters.chosen_contract_mismatch, 1); - assert.equal(result.rejectionClusters.no_action_dominates, 1); - }); - - it("recognizes the protected rejection classes", () => { - assert.deepEqual([...SEED43_PREFERENCE_REJECTION_CLASSES], [ - "wrong_tool", - "wrong_arguments_or_effects", - "missing_continuation", - "false_positive_mutation", - "collapse_sentinel", - ]); - }); -}); From cfab611add2f56d96142751a75a5c031aada030d Mon Sep 17 00:00:00 2001 From: Luis Manrique Date: Wed, 5 Aug 2026 04:32:54 +0000 Subject: [PATCH 3/4] test: cover preference admission sign semantics --- tests/preference-admission.test.mjs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/preference-admission.test.mjs b/tests/preference-admission.test.mjs index 8d41daf4..a3b12a0a 100644 --- a/tests/preference-admission.test.mjs +++ b/tests/preference-admission.test.mjs @@ -114,6 +114,7 @@ describe("preference admission", () => { requiredActionFamilies: ["read", "write"], requireMultiEffectContinuation: true, requiredRejectionClasses: [...PREFERENCE_REJECTION_CLASSES], + desiredRejectionClasses: ["wrong_arguments_or_effects"], })); assert.equal(result.admitted, false); assert.match(result.rejections.map((item) => item.reason).join(","), /rejection_class_mismatch/); @@ -121,6 +122,7 @@ describe("preference admission", () => { assert.match(globals, /missing_action_family:read/); assert.match(globals, /missing_multi_effect_continuation/); assert.match(globals, /missing_rejection_class:collapse_sentinel/); + assert.deepEqual(result.missingDesiredRejectionClasses, ["wrong_arguments_or_effects"]); }); it("accepts the no-action sentinel when the row contract is no-action", () => { From 899ce0b5f9db458f7adf3cf60121bfa20a44aa32 Mon Sep 17 00:00:00 2001 From: Luis Manrique Date: Wed, 5 Aug 2026 04:51:21 +0000 Subject: [PATCH 4/4] fix: close preference admission duplicate gaps --- src/preference-admission.ts | 10 +--------- tests/preference-admission.test.mjs | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/preference-admission.ts b/src/preference-admission.ts index 0a456f91..95137cb5 100644 --- a/src/preference-admission.ts +++ b/src/preference-admission.ts @@ -176,7 +176,7 @@ export function admitPreferencePairs( if (pairIds.has(pair.pairId)) addRejection(result, pair, "duplicate_pair_id"); pairIds.add(pair.pairId); - if (!config.allowMultipleRejectionsPerRow && rowIds.get(pair.rowId)?.length === 2) { + if (!config.allowMultipleRejectionsPerRow && (rowIds.get(pair.rowId)?.length ?? 0) >= 2) { addRejection(result, pair, "duplicate_source_row_id"); } if (pair.split !== "train" || !config.allowedTrainRowIds.has(pair.rowId)) { @@ -212,14 +212,6 @@ export function admitPreferencePairs( && pair.chosenTokens[2] === 11) { addRejection(result, pair, "chosen_collapse_sentinel"); } - if (pair.actionRequired - && pair.chosenTokens.length === 3 - && pair.chosenTokens[0] === 16071 - && pair.chosenTokens[1] === 95597 - && pair.chosenTokens[2] === 11) { - addRejection(result, pair, "chosen_collapse_sentinel"); - } - const verified = config.verifyRejectedPayload(pair); if (verified === null) { addRejection(result, pair, "missing_rejected_payload_preimage"); diff --git a/tests/preference-admission.test.mjs b/tests/preference-admission.test.mjs index a3b12a0a..ecd2950b 100644 --- a/tests/preference-admission.test.mjs +++ b/tests/preference-admission.test.mjs @@ -95,18 +95,38 @@ describe("preference admission", () => { const result = admitPreferencePairs([ pair({ pairId: "duplicate", split: "dev", runtimeSha256: "d".repeat(64) }), pair({ pairId: "duplicate", rowId: "row-2", sourceCapabilitySha256: "e".repeat(64) }), + pair({ pairId: "duplicate-3" }), ], config(() => verified({ semantic: { kind: "tool_calls", toolCalls: [{ name: "write", arguments: { id: "x" }, effects: ["updated:x"] }] }, }), { requireMultiEffectContinuation: false })); assert.equal(result.admitted, false); const reasons = result.rejections.map((item) => item.reason).join(","); assert.match(reasons, /duplicate_pair_id/); + assert.equal(result.rejections.filter((item) => item.reason === "duplicate_source_row_id").length, 1); assert.match(reasons, /row_outside_frozen_train_allowlist/); assert.match(reasons, /runtime_mismatch/); assert.match(reasons, /source_capability_mismatch/); assert.match(reasons, /identical_chosen_rejected_semantics/); }); + it("rejects the collapse sentinel for action-required rows", () => { + const result = admitPreferencePairs([ + pair({ chosenTokens: [16071, 95597, 11] }), + ], config(() => verified(), { requireMultiEffectContinuation: false })); + assert.equal(result.admitted, false); + assert.equal(result.rejections.filter((item) => item.reason === "chosen_collapse_sentinel").length, 1); + }); + + it("rejects every source-row occurrence after the first", () => { + const result = admitPreferencePairs([ + pair({ pairId: "row-dup-1" }), + pair({ pairId: "row-dup-2" }), + pair({ pairId: "row-dup-3" }), + ], config(() => verified(), { requireMultiEffectContinuation: false })); + assert.equal(result.admitted, false); + assert.equal(result.rejections.filter((item) => item.reason === "duplicate_source_row_id").length, 2); + }); + it("rejects caller-mislabeled classes and reports global coverage failures", () => { const result = admitPreferencePairs([ pair({ expectedRejectionClass: "false_positive_mutation" }),