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
91 changes: 90 additions & 1 deletion packages/headless/src/__tests__/ab-manifest.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,13 @@ import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, test } from 'node:test';
import { buildAbRunManifest, ensureAbRunManifest, readAbRunManifest } from '../ab-manifest.js';
import {
buildAbRunManifest,
buildRunManifestFingerprint,
canonicalJson,
ensureAbRunManifest,
readAbRunManifest,
} from '../ab-manifest.js';
import { sha256 } from './helpers/hash-fixture.js';

describe('buildAbRunManifest', () => {
Expand DownExpand Up@@ -152,3 +158,86 @@ describe('buildAbRunManifest', () => {
}
});
});

// `canonicalJson` and `buildRunManifestFingerprint` are the load-bearing
// determinism contract for A/B resume identities. These behavior tests pin the
// invariants (per #1403): key-order independence, `undefined`-field dropping,
// array-order preservation, and fingerprint stability — rather than asserting on
// the exact serialized bytes, which would over-constrain the implementation.
describe('canonicalJson', () => {
test('drops undefined object fields so {a:1,b:undefined} equals {a:1}', () => {
assert.equal(canonicalJson({ a: 1, b: undefined }), canonicalJson({ a: 1 }));
assert.equal(canonicalJson({ a: 1, b: undefined }), '{"a":1}');
});

test('is independent of object key insertion order', () => {
assert.equal(canonicalJson({ b: 2, a: 1 }), canonicalJson({ a: 1, b: 2 }));
});

test('preserves array order (order is semantically meaningful)', () => {
assert.equal(canonicalJson([3, 1, 2]), '[3,1,2]');
assert.notEqual(canonicalJson([3, 1, 2]), canonicalJson([1, 2, 3]));
});

test('recursively sorts keys in nested objects', () => {
assert.equal(canonicalJson({ outer: { z: 1, a: 2 } }), '{"outer":{"a":2,"z":1}}');
});

test('serializes scalars via JSON.stringify', () => {
assert.equal(canonicalJson(null), 'null');
assert.equal(canonicalJson('hi'), '"hi"');
assert.equal(canonicalJson(42), '42');
assert.equal(canonicalJson(true), 'true');
});
});

describe('buildRunManifestFingerprint', () => {
test('produces the same fingerprint for logically equal payloads regardless of key order', () => {
assert.equal(
buildRunManifestFingerprint({ b: 2, a: 1 }),
buildRunManifestFingerprint({ a: 1, b: 2 }),
);
});

test('is stable across repeated calls (idempotent)', () => {
const payload = { arms: [{ id: 'on' }, { id: 'off' }], reps: 3 };
assert.equal(buildRunManifestFingerprint(payload), buildRunManifestFingerprint(payload));
});

test('ignores undefined fields when computing the fingerprint', () => {
assert.equal(
buildRunManifestFingerprint({ a: 1, b: undefined }),
buildRunManifestFingerprint({ a: 1 }),
);
});

// Regression guard for the harness-oracle-registry divergence: the registry
// path recomputes a fingerprint over entry/snapshot bodies and compares it to
// a stored value. Before unification, harness-oracle-registry.ts carried a
// local canonicalJson copy that omitted the undefined-field filter, so a body
// containing an undefined-valued nested field would hash to invalid JSON
// ({"field":undefined}) and never match the canonical computation. This pins
// the contract through buildRunManifestFingerprint using a registry-shaped
// payload with a nested undefined field.
test('registry-shaped body with a nested undefined field matches the all-defined body', () => {
const allDefined = {
schemaVersion: 1,
taskId: 'task-a',
identity: { taskFingerprint: 'sha256:aaa', executionPolicyFingerprint: 'sha256:bbb' },
execution: { status: 'completed' },
oracle: { outcome: 'passed', reward: 1, attempts: 1 },
};
const withUndefined = {
...allDefined,
identity: { ...allDefined.identity, evidenceFingerprint: undefined },
};
assert.equal(
buildRunManifestFingerprint(withUndefined),
buildRunManifestFingerprint(allDefined),
);
});

test('yields a sha256:<64 lowercase hex> string', () => {
assert.match(buildRunManifestFingerprint({ a: 1 }), /^sha256:[a-f0-9]{64}$/);
});
});
29 changes: 9 additions & 20 deletions packages/headless/src/__tests__/harness-oracle-registry.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, test } from 'node:test';
import { buildRunManifestFingerprint } from '../ab-manifest.js';
import {
auditHarnessOracleRegistry,
HarnessOracleAuditExecutionError,
Expand DownExpand Up@@ -298,8 +298,10 @@ describe('harness Oracle evidence registry', () => {
});
const invalid = structuredClone(baseline.snapshot);
invalid.entries[0]!.oracle!.reward = 0;
invalid.entries[0]!.fingerprint = fingerprintFixture(withoutFingerprint(invalid.entries[0]!));
invalid.fingerprint = fingerprintFixture(withoutFingerprint(invalid));
invalid.entries[0]!.fingerprint = buildRunManifestFingerprint(
withoutFingerprint(invalid.entries[0]!),
);
invalid.fingerprint = buildRunManifestFingerprint(withoutFingerprint(invalid));

assert.throws(
() => planHarnessOracleRegistryAudit(tasks, invalid),
Expand All@@ -308,10 +310,12 @@ describe('harness Oracle evidence registry', () => {

const excessiveAttempts = structuredClone(baseline.snapshot);
excessiveAttempts.entries[0]!.oracle!.attempts = HARBOR_ORACLE_MAX_ATTEMPTS + 1;
excessiveAttempts.entries[0]!.fingerprint = fingerprintFixture(
excessiveAttempts.entries[0]!.fingerprint = buildRunManifestFingerprint(
withoutFingerprint(excessiveAttempts.entries[0]!),
);
excessiveAttempts.fingerprint = fingerprintFixture(withoutFingerprint(excessiveAttempts));
excessiveAttempts.fingerprint = buildRunManifestFingerprint(
withoutFingerprint(excessiveAttempts),
);
assert.throws(
() => planHarnessOracleRegistryAudit(tasks, excessiveAttempts),
/registry entry is malformed/,
Expand DownExpand Up@@ -536,18 +540,3 @@ function oracleExecutionProvenance(runId: string) {
},
};
}

function fingerprintFixture(value: unknown): string {
return `sha256:${createHash('sha256').update(canonicalJsonFixture(value)).digest('hex')}`;
}

function canonicalJsonFixture(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalJsonFixture).join(',')}]`;
if (value && typeof value === 'object') {
return `{${Object.entries(value)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJsonFixture(item)}`)
.join(',')}}`;
}
return JSON.stringify(value);
}
15 changes: 2 additions & 13 deletions packages/headless/src/__tests__/prompt-ab-manifest.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
import assert from 'node:assert/strict';
import { createHash } from 'node:crypto';
import { writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { describe, test } from 'node:test';
import { buildRunManifestFingerprint } from '../ab-manifest.js';
import { buildPromptAbRunManifest, ensurePromptAbRunManifest } from '../prompt-ab-manifest.js';
import type { PromptAbRunManifestInput } from '../prompt-ab-types.js';
import { sha256 } from './helpers/hash-fixture.js';
Expand DownExpand Up@@ -201,21 +201,10 @@ function buildLegacyPromptAbRunManifest(
});
return {
...manifestWithoutFingerprint,
fingerprint: `sha256:${createHash('sha256').update(canonicalJson(manifestWithoutFingerprint)).digest('hex')}`,
fingerprint: buildRunManifestFingerprint(manifestWithoutFingerprint),
};
}

function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
if (value && typeof value === 'object') {
const entries = Object.entries(value)
.filter(([, entryValue]) => entryValue !== undefined)
.sort(([a], [b]) => a.localeCompare(b));
return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`).join(',')}}`;
}
return JSON.stringify(value);
}

function withoutUndefined<T extends Record<string, unknown>>(value: T): T {
return Object.fromEntries(
Object.entries(value).filter(([, entryValue]) => entryValue !== undefined),
Expand Down
14 changes: 13 additions & 1 deletion packages/headless/src/ab-manifest.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -104,7 +104,19 @@ function hasFullBodyFingerprint(
return schemaVersion === 'maka.ab.run_manifest.v1';
}

function canonicalJson(value: unknown): string {
/**
* Deterministic JSON serializer shared across `@maka/headless`: drops `undefined`
* object fields and recursively sorts object keys, so that two logically equal
* objects always serialize to the same bytes. Arrays preserve order (order is
* semantically meaningful). Every resume-identity and integrity fingerprint in
* the package goes through here via `buildRunManifestFingerprint`, so that
* canonicalization never drifts across call sites.
*
* Note: `harness-oracle-policy.ts` keeps its own `JSON.stringify`-based
* `fingerprintValue` for the execution-policy fingerprint, deliberately not
* canonicalized (its inputs are a single frozen `as const` object).
*/
export function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
if (value && typeof value === 'object') {
const entries = Object.entries(value)
Expand Down
12 changes: 1 addition & 11 deletions packages/headless/src/ab-summary.ts
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
import { canonicalJson } from './ab-manifest.js';
import { BUDGET_EXHAUSTED_RUNTIME_UNAVAILABLE_REASON } from './fixed-prompt-controller.js';
import type { FixedPromptTaskWalEvent } from './fixed-prompt-wal-types.js';
import type { HarborCellTokenSummary } from './cell-output.js';
Expand DownExpand Up@@ -842,17 +843,6 @@ function clampRateDelta(value: number): number {
return Math.min(1, Math.max(-1, value));
}

function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
if (value && typeof value === 'object') {
const entries = Object.entries(value)
.filter(([, entryValue]) => entryValue !== undefined)
.sort(([a], [b]) => a.localeCompare(b));
return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`).join(',')}}`;
}
return JSON.stringify(value);
}

function assertSameRunCount(
baselineRuns: readonly unknown[],
candidateRuns: readonly unknown[],
Expand Down
29 changes: 7 additions & 22 deletions packages/headless/src/harness-oracle-registry.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
import { createHash } from 'node:crypto';
import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { buildRunManifestFingerprint } from './ab-manifest.js';
import type { FixedPromptTask } from './fixed-prompt-controller.js';
import { fingerprintFixedPromptTask } from './fixed-prompt-task-source.js';
import {
Expand DownExpand Up@@ -233,7 +233,7 @@ export function buildHarnessOracleEnvironmentFingerprint(
baseImages.some((image) => image.reference.length === 0 || image.digest.length === 0)
)
throw new Error('Oracle environment identity is malformed');
return fingerprintValue({
return buildRunManifestFingerprint({
schemaVersion: 1,
environment: input.environment,
platform: input.platform,
Expand DownExpand Up@@ -368,7 +368,7 @@ export function parseHarnessOracleRegistrySnapshot(value: unknown): HarnessOracl
}

export function fingerprintHarnessOracleDocument(value: unknown): string {
return fingerprintValue(value);
return buildRunManifestFingerprint(value);
}

function annotationState(entry: HarnessOracleRegistryEntry): HarnessOracleAnnotationState {
Expand All@@ -380,12 +380,12 @@ function annotationState(entry: HarnessOracleRegistryEntry): HarnessOracleAnnota
}

function qualificationKeyFor(taskId: string, identity: HarnessOracleQualificationIdentity): string {
return fingerprintValue({ schemaVersion: 1, taskId, identity });
return buildRunManifestFingerprint({ schemaVersion: 1, taskId, identity });
}

function assertSnapshotFingerprint(snapshot: HarnessOracleRegistrySnapshot): void {
const { fingerprint, ...body } = snapshot;
if (fingerprint !== fingerprintValue(body)) {
if (fingerprint !== buildRunManifestFingerprint(body)) {
throw new Error('Oracle registry snapshot fingerprint is invalid');
}
if (
Expand DownExpand Up@@ -440,7 +440,7 @@ function registryEntryIsValid(
if (
entry.schemaVersion !== 1 ||
entry.taskId !== expectedTaskId ||
entry.fingerprint !== fingerprintValue(withoutFingerprint(entry)) ||
entry.fingerprint !== buildRunManifestFingerprint(withoutFingerprint(entry)) ||
entry.qualificationKey !== qualificationKeyFor(entry.taskId, entry.identity) ||
!qualificationIdentityIsValid(entry.identity) ||
!executionProvenanceIsValid(entry.executionProvenance)
Expand DownExpand Up@@ -524,22 +524,7 @@ function withoutFingerprint<T extends { fingerprint: string }>(value: T): Omit<T
}

function withFingerprint<T extends Record<string, unknown>>(body: T): T & { fingerprint: string } {
return { ...body, fingerprint: fingerprintValue(body) };
}

function fingerprintValue(value: unknown): string {
return `sha256:${createHash('sha256').update(canonicalJson(value)).digest('hex')}`;
}

function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
if (isRecord(value)) {
return `{${Object.entries(value)
.sort(([a], [b]) => a.localeCompare(b))
.map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
.join(',')}}`;
}
return JSON.stringify(value);
return { ...body, fingerprint: buildRunManifestFingerprint(body) };
}

function isRecord(value: unknown): value is Record<string, unknown> {
Expand Down
18 changes: 2 additions & 16 deletions packages/headless/src/kimi-protocol-ab.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
import { createHash } from 'node:crypto';
import type { ModelInfo } from '@maka/core';
import { renderAbComparisonMarkdown } from './ab-render.js';
import { buildRunManifestFingerprint, canonicalJson } from './ab-manifest.js';
import { buildAbRoundId, runAbComparison } from './ab-run.js';
import { summarizeAbComparison } from './ab-summary.js';
import type { AbArmSpec, AbComparisonSummary } from './ab-types.js';
Expand DownExpand Up@@ -634,9 +635,7 @@ function protocolArmSpec(arm: (typeof KIMI_PROTOCOL_ARMS)[number]): AbArmSpec {
}

function armResumeFingerprint(base: string | undefined, protocol: KimiProtocol): string {
return `sha256:${createHash('sha256')
.update(canonicalJson({ version: 1, base: base ?? null, protocol }))
.digest('hex')}`;
return buildRunManifestFingerprint({ version: 1, base: base ?? null, protocol });
}

function requireCaptures(
Expand All@@ -662,16 +661,3 @@ function mean(values: readonly number[]): number | null {
? values.reduce((total, value) => total + value, 0) / values.length
: null;
}

function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
if (value && typeof value === 'object') {
const entries = Object.entries(value)
.filter(([, entry]) => entry !== undefined)
.sort(([left], [right]) => left.localeCompare(right));
return `{${entries
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalJson(entry)}`)
.join(',')}}`;
}
return JSON.stringify(value);
}
20 changes: 6 additions & 14 deletions packages/headless/src/prompt-ab-manifest.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
import { createHash } from 'node:crypto';
import { ensureAbRunManifest, buildAbRunManifest } from './ab-manifest.js';
import {
ensureAbRunManifest,
buildAbRunManifest,
buildRunManifestFingerprint,
} from './ab-manifest.js';
import type { PromptAbRunManifest, PromptAbRunManifestInput } from './prompt-ab-types.js';

export function buildPromptAbRunManifest(input: PromptAbRunManifestInput): PromptAbRunManifest {
Expand DownExpand Up@@ -68,7 +71,7 @@ export function buildPromptAbRunManifest(input: PromptAbRunManifestInput): Promp
...promptManifestWithoutFingerprint,
experimentKind: 'prompt',
arms: genericManifest.arms as PromptAbRunManifest['arms'],
fingerprint: `sha256:${createHash('sha256').update(canonicalJson(promptManifestWithoutFingerprint)).digest('hex')}`,
fingerprint: buildRunManifestFingerprint(promptManifestWithoutFingerprint),
};
}

Expand DownExpand Up@@ -98,17 +101,6 @@ export async function ensurePromptAbRunManifest(
}
}

function canonicalJson(value: unknown): string {
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
if (value && typeof value === 'object') {
const entries = Object.entries(value)
.filter(([, entryValue]) => entryValue !== undefined)
.sort(([a], [b]) => a.localeCompare(b));
return `{${entries.map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`).join(',')}}`;
}
return JSON.stringify(value);
}

function withoutUndefined<T extends Record<string, unknown>>(value: T): T {
return Object.fromEntries(
Object.entries(value).filter(([, entryValue]) => entryValue !== undefined),
Expand Down
Loading
Loading