mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
0b3cfb77d6
* feat(metaharness): repair dependency contract + strict sequential promotion evidence Item 1 — dependency & compatibility repair (the contract was silently broken): - @metaharness/darwin ^0.8.3, @metaharness/flywheel ^0.1.10, and @metaharness/radio ^0.1.0 are now explicit optionalDependencies of @claude-flow/cli (optional PEER deps are never auto-installed, so a clean ruflo install shipped with zero MetaHarness packages on disk) - check-metaharness-pins.mjs now searches dependencies, optionalDependencies, AND peerDependencies; UNDECLARED and PEER-ONLY (for installable pins) are fatal drift instead of silently passing; radio added to the watch list; --require-installed asserts the real published symbol contracts (RefineMutator, withSequentialEvidence, RadioBus, ...) - new scripts/metaharness-clean-install-test.mjs: installs the declared ranges into a pristine temp dir and asserts every advertised export; wired as a MANDATORY clean-install job in metaharness-ci.yml - doctor: new "MetaHarness declared packages" check FAILS (not warns) when a declared optional dep does not resolve at runtime; --component metaharness now runs upstream + declared-deps + integration checks - MH_DARWIN_PIN bumped 0.8.0 → 0.8.3 in lock-step with the declared range Item 2 — strict promotion evidence at the transaction authority: - receipts now carry task-level pairedOutcomes (taskId + per-task baseline/ candidate scores) behind heldOutDeltas; verifyFlywheelReceipt refuses rows that cannot reproduce their aggregate; evaluateFlywheelCandidate populates them; pre-existing receipts still verify byte-identically - new flywheel-sequential-evidence.ts: anytime-valid e-process over discordant paired outcomes (testing-by-betting) composed with per-candidate alpha allocation (alpha_k = alpha_total * 6/(pi^2 k^2)), so the family-wise false-promotion probability across an ADAPTIVE candidate stream is bounded by alpha_total = 5% - promoteFlywheelCandidate requires paired evidence by DEFAULT — aggregate- only receipts are refused, never silently downgraded (the upstream withSequentialEvidence fallback hole); explicit --allow-aggregate-evidence / allowAggregateEvidence escape hatch for pre-upgrade receipts; alpha spend is persisted per receiptId in the transaction state (looking spends alpha; retries reuse their index) - acceptance test: 1,000 null-improvement streams (20 adaptive candidates x 40 worst-case discordant pairs) — measured family-wise false promotion 0.6%, within the <=5% budget Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01FKWLVWewYaWmcUg2q8Y9wy * chore: gitignore node-compile-cache build artifact Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01FKWLVWewYaWmcUg2q8Y9wy * fix(ci): satisfy #2561 guard, dep-overlap audit, and smoke contract for metaharness optional deps Three CI failures from the dependency-contract repair, three fixes: - tilde-pin @metaharness/{darwin,flywheel,radio} (~0.8.3 / ~0.1.10 / ~0.1.0) per the ADR-150 anti-caret rule enforced by smoke step 17z73 — upstream is unstable, tilde absorbs patches but never minors - remove the three from peerDependencies/peerDependenciesMeta: an entry in BOTH optionalDependencies and peerDependencies crashes npm 11.x arborist on dedupe (#1147/#2018 dep-overlap audit); optionalDependencies alone is the declaration that actually installs - update the #2561 cold-startup guard per its own escape clause: budget 8 → 10 and drop @metaharness/darwin from the forbidden list — that entry dated from the pre-0.8 heavy-tree era; darwin@0.8.3 is 1.8M unpacked with ZERO dependencies and no lifecycle scripts (all three packages combined: 2.3M, 753ms cold install into an empty dir, measured 2026-08-10; the clean-install CI job re-verifies on every pin change) - smoke.sh 17h now accepts the componentMap ARRAY form ('metaharness': [checkMetaharness, ...]) introduced with the declared-packages doctor check Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01FKWLVWewYaWmcUg2q8Y9wy * fix(ci): remove apostrophe that broke the single-quoted guard script The #2561 guard runs as `node -e '...'` inside bash; an apostrophe in a comment ("guard's") terminated the quoted string and bash tried to execute the next // comment line (exit 126, '//: Is a directory'). Verified by executing the extracted run block end-to-end: all three checks OK, exit 0. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01FKWLVWewYaWmcUg2q8Y9wy * feat(flywheel): ADR-381 — alpha-stream governance, reset epochs, accept/v2+seq Completes the sequential-evidence governance PR #2956 opened: ADR-381 (new) records the decisions: - the ADR-322 promotion ledger IS the alpha stream (per project root) — receipt lineageIds default to fresh UUIDs and cannot scope the control - evidence epochs: resetSequentialEvidence({confirm, reason}) archives the spend into an append-only sequentialResets audit trail, EXPIRES every outstanding evaluated receipt (fresh-data enforcement — old-epoch evidence cannot be replayed against a reopened budget), and increments evidenceEpoch; surfaced as `flywheel evidence-reset --reason … --confirm` (CLI) and the metaharness_flywheel MCP op, both behind the same policy gate as promotion - accept/v2+seq: the ADR-176 generations loop now decides each bundle with a third conjunct — the e-process over the bundle's own embedded per-task holdout at alpha_k for its position in the attempts stream — with the full verdict recorded in the bundle and independently replayed by verifyReceiptBundle (v1 bundles keep v1 semantics; versions pin per bundle); flywheelStatus surfaces next test index / threshold / minimum pairs / remaining budget so exhaustion reads as plateau, not mystery - two-layer pre-flight: evaluateFlywheelCandidate annotates (never blocks) when the promotion holdout cannot clear the next threshold on a perfect sweep; promoteFlywheelCandidate refuses size-inviable receipts BEFORE allocating an alpha index — sample size is ancillary, so the refusal looks at no evidence and spends no budget Tests: reset semantics (archive/expire/epoch/fresh-promote), alpha-free size refusal vs alpha-spending e-process refusal, v2 promote/reject/replay including tamper + index-shopping detection, v1 compat, helper math anchors (min pairs = 9 at test 1), budget monotonicity. 82 tests green across the eight affected suites; tsc build clean. Co-Authored-By: RuFlo <ruv@ruv.net> Claude-Session: https://claude.ai/code/session_01FKWLVWewYaWmcUg2q8Y9wy * fix(flywheel): close 3 concurrency/epoch gaps in ADR-381 sequential evidence Code review of this PR found three real correctness bugs that can silently violate the <=5% family-wise false-promotion guarantee that is the PR's own core deliverable: - flywheel-transaction.ts: resetSequentialEvidence's "fresh data only" guarantee only expired receipts already registered at reset time; a receipt whose evidence predates a reset but is registered afterward was silently admitted into the new, cheaper epoch (index shopping). Now tracks evidenceEpochStartedAt and promoteFlywheelCandidate refuses any receipt whose payload.issuedAt predates it, before allocating an alpha index. - harness-flywheel-generations.ts: the daemon generations loop computed its sequential-evidence testIndex via an unlocked loadAttempts(root).length+1 read before appending. Two overlapping runFlywheelGeneration calls on the same root could be assigned the same test index and spend the same alpha_k twice. The read-index -> build-bundle -> append critical section now runs under the same O_EXCL lock pattern flywheel-transaction.ts already uses. - harness-flywheel.ts: evaluateFlywheelCandidate's sequentialPreflight (backing the `promotable` flag) was computed from an unlocked snapshot taken before the async retrieval/scoring work, so it could go stale by the time promoteFlywheelCandidate allocates the real index under its own lock. Moved to the latest possible read (after receipt registration) to minimize the window; documented as advisory, since promoteFlywheelCandidate remains the sole authority. Added a regression test for the epoch-boundary refusal and a concurrency test proving two racing generations get distinct sequential test indices. Co-Authored-By: RuFlo <ruv@ruv.net> --------- Co-authored-by: Claude <noreply@anthropic.com>
290 lines
15 KiB
TypeScript
290 lines
15 KiB
TypeScript
/**
|
|
* single-round proof-of-mechanism (ADR-176) — NOT flywheel/compounding/production
|
|
* proof. Verifies: the 7 required artifacts are emitted, the real versioned
|
|
* accept() gate decides, the bundle is independently replayable (no service
|
|
* logs), a pass registers in SHADOW (never served), and a lineage reconstructs.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
runSyntheticProofRound, runRealEvolveRound, verifyReceiptBundle, reconstructLineage,
|
|
mutationEffectiveness, detectPlateau, classifyMutation,
|
|
PROMOTION_RULE_VERSION, PROOF_LABEL,
|
|
} from '../src/services/evolve-proof.js';
|
|
|
|
describe('runSyntheticProofRound — required artifacts + gate wiring', () => {
|
|
const b = runSyntheticProofRound({ now: 1000 });
|
|
|
|
it('emits all seven required artifacts', () => {
|
|
expect(b.inputHoldoutHash).toMatch(/^sha256:/);
|
|
expect(b.baselineManifestHash).toMatch(/^sha256:/);
|
|
expect(b.candidateManifestHash).toMatch(/^sha256:/);
|
|
expect(b.meetsPromotionRule.version).toBe(PROMOTION_RULE_VERSION); // meetsPromotionRule version
|
|
expect(b.decisionReceipt.result.terms).toBeTruthy(); // decision receipt
|
|
expect(b.shadow?.registrationId).toMatch(/^shadow:/); // SHADOW registration id
|
|
expect(b.costReceipt).toEqual({ usd: 0, llmCalls: 0, tier: 'synthetic', notes: expect.any(String) }); // cost receipt
|
|
});
|
|
|
|
it('is labeled exactly, and carries the anti-marketing disclaimers', () => {
|
|
expect(b.label).toBe(PROOF_LABEL);
|
|
expect(b.label).toBe('single-round proof-of-mechanism');
|
|
expect(b.disclaimers).toEqual(['not flywheel proof', 'not compounding learning', 'not production learning']);
|
|
});
|
|
|
|
it('the REAL versioned accept() decided promotion (gate wiring)', () => {
|
|
expect(b.decisionReceipt.promotionRuleVersion).toBe(PROMOTION_RULE_VERSION);
|
|
expect(b.decisionReceipt.promoted).toBe(b.meetsPromotionRule.result);
|
|
});
|
|
|
|
it('a pass registers in SHADOW and is NOT served (no auto-serve path)', () => {
|
|
expect(b.meetsPromotionRule.result).toBe(true);
|
|
expect(b.shadow).not.toBeNull();
|
|
expect(b.shadow!.state).toBe('shadow');
|
|
expect(b.shadow!.served).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('verifyReceiptBundle — independent replay (no service logs)', () => {
|
|
it('independently recomputes the decision and confirms the pass', () => {
|
|
const b = runSyntheticProofRound({ now: 1 });
|
|
const v = verifyReceiptBundle(b);
|
|
expect(v.valid).toBe(true);
|
|
expect(v.hashChecks).toEqual({ inputHoldout: true, baselineManifest: true, candidateManifest: true });
|
|
expect(v.decisionMatches).toBe(true);
|
|
expect(v.noAutoServe).toBe(true);
|
|
expect(v.explanation).toMatch(/PASS under accept\/v1/);
|
|
});
|
|
|
|
it('detects a tampered holdout (hash mismatch → invalid)', () => {
|
|
const b = runSyntheticProofRound({ now: 1 });
|
|
b.holdout[0].candidateScore = 0.999; // tamper after hashing
|
|
const v = verifyReceiptBundle(b);
|
|
expect(v.valid).toBe(false);
|
|
expect(v.mismatches).toContain('input holdout hash mismatch');
|
|
});
|
|
|
|
it('detects a forged decision (recorded promoted != recomputed)', () => {
|
|
const b = runSyntheticProofRound({ now: 1 });
|
|
b.meetsPromotionRule.result = false; // lie about the outcome
|
|
const v = verifyReceiptBundle(b);
|
|
expect(v.decisionMatches).toBe(false);
|
|
expect(v.valid).toBe(false);
|
|
});
|
|
|
|
it('flags an auto-served candidate (served=true violates shadow-only)', () => {
|
|
const b = runSyntheticProofRound({ now: 1 });
|
|
(b.shadow as unknown as { served: boolean }).served = true;
|
|
expect(verifyReceiptBundle(b).noAutoServe).toBe(false);
|
|
});
|
|
|
|
it('REJECT path: a regressing candidate fails the gate and does NOT register shadow', () => {
|
|
const b = runSyntheticProofRound({ now: 1, regress: true });
|
|
expect(b.meetsPromotionRule.result).toBe(false);
|
|
expect(b.shadow).toBeNull();
|
|
const v = verifyReceiptBundle(b);
|
|
expect(v.valid).toBe(true); // the rejection is itself replayable
|
|
expect(v.explanation).toMatch(/FAIL under accept\/v1/);
|
|
});
|
|
});
|
|
|
|
describe('runRealEvolveRound — measured round, same gate + replayability as synthetic', () => {
|
|
const holdout = [
|
|
{ taskId: 'q05', baselineScore: 0.50, candidateScore: 0.62 },
|
|
{ taskId: 'q06', baselineScore: 0.80, candidateScore: 0.86 },
|
|
{ taskId: 'q07', baselineScore: 0.70, candidateScore: 0.78 },
|
|
{ taskId: 'q08', baselineScore: 0.60, candidateScore: 0.70 },
|
|
{ taskId: 'q09', baselineScore: 0.72, candidateScore: 0.80 },
|
|
];
|
|
const baseline = { alpha: 0.5, subjectWeight: 2, mmrLambda: 0.7, bodyWeight: 1, typePenaltyFactor: 1 };
|
|
const candidate = { alpha: 0.3, subjectWeight: 1, mmrLambda: 0.5, bodyWeight: 1.5, typePenaltyFactor: 0.5 };
|
|
|
|
it('a real measured improvement promotes, is kind:real, and replays independently', () => {
|
|
const b = runRealEvolveRound({ baseline, candidate, holdout, generation: 0, parent: null, now: 1, redblue: 'PASS', corpus: 'ADR-081-frozen-v1' });
|
|
expect(b.kind).toBe('real');
|
|
expect(b.decisionReceipt.promoted).toBe(true);
|
|
expect(b.costReceipt.usd).toBe(0); // $0 path
|
|
expect(b.shadow?.served).toBe(false); // shadow, not served
|
|
const v = verifyReceiptBundle(b);
|
|
expect(v.valid).toBe(true); // independently replayable
|
|
expect(v.explanation).toMatch(/PASS under accept\/v1/);
|
|
});
|
|
|
|
it('a real round that regresses train (redblue FAIL) is rejected', () => {
|
|
const b = runRealEvolveRound({ baseline, candidate, holdout, generation: 0, parent: null, now: 1, redblue: 'FAIL', corpus: 'x' });
|
|
expect(b.decisionReceipt.promoted).toBe(false);
|
|
expect(b.regression?.failureCause).toBe('security'); // redblue term
|
|
expect(verifyReceiptBundle(b).valid).toBe(true); // rejection still replayable
|
|
});
|
|
|
|
it('two real rounds chain (winner→next baseline) into an intact lineage', () => {
|
|
const g0 = runRealEvolveRound({ baseline, candidate, holdout, generation: 0, parent: null, now: 1, corpus: 'x' });
|
|
const g1 = runRealEvolveRound({ baseline: candidate, candidate: { ...candidate, alpha: 0.2 }, holdout, generation: 1, parent: g0.candidateManifestHash, now: 2, corpus: 'x' });
|
|
const t = reconstructLineage([g0, g1]);
|
|
expect(t.promotions).toBe(2);
|
|
expect(t.lineageIntact).toBe(true);
|
|
expect(t.allReplayable).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('causality — the record explains WHY, not just what', () => {
|
|
it('a promotion carries a mutation class, summary, and multi-dimensional deltas', () => {
|
|
const b = runSyntheticProofRound({ now: 1 });
|
|
expect(b.mutationClass).toBe('retrieval:multi'); // 5 knobs changed
|
|
expect(b.mutationSummary).toMatch(/alpha:0.5→0.3/);
|
|
expect(b.deltas.benchmark).toBeGreaterThan(0);
|
|
expect(b.deltas.security).toBe(0);
|
|
expect(b.promotion).not.toBeNull();
|
|
expect(b.regression).toBeNull();
|
|
expect(verifyReceiptBundle(b).causalConsistent).toBe(true);
|
|
});
|
|
|
|
it('a rejection records failure cause + ancestor (regression ancestry)', () => {
|
|
const b = runSyntheticProofRound({ now: 1, regress: true });
|
|
expect(b.promotion).toBeNull();
|
|
expect(b.regression).not.toBeNull();
|
|
expect(b.regression!.failureCause).toBe('canary'); // regressing task → canary_no_worse fails first
|
|
expect(b.regression!.failedTerms.length).toBeGreaterThan(0);
|
|
expect(b.regression!.ancestor).toBeTruthy();
|
|
});
|
|
|
|
it('classifyMutation names single-knob vs multi-knob changes', () => {
|
|
expect(classifyMutation({ alpha: 0.5, x: 1 }, { alpha: 0.3, x: 1 }).mutationClass).toBe('retrieval:alpha');
|
|
expect(classifyMutation({ alpha: 0.5, x: 1 }, { alpha: 0.3, x: 2 }).mutationClass).toBe('retrieval:multi');
|
|
});
|
|
});
|
|
|
|
// helper: build a linear promoted chain of `n` generations
|
|
function chainOf(n: number, branch = 'main'): ReturnType<typeof runSyntheticProofRound>[] {
|
|
const cfg = (a: number) => ({ alpha: a, subjectWeight: 2, mmrLambda: 0.7, bodyWeight: 1, typePenaltyFactor: 1 });
|
|
const out = [];
|
|
let parent: string | null = null, base = cfg(0.9);
|
|
for (let g = 0; g < n; g++) {
|
|
const cand = cfg(+(0.9 - (g + 1) * 0.01).toFixed(3));
|
|
const b = runSyntheticProofRound({ now: g + 1, generation: g, parent, branch, baseline: base, candidate: cand });
|
|
out.push(b); parent = b.candidateManifestHash; base = cand;
|
|
}
|
|
return out;
|
|
}
|
|
|
|
describe('reconstructLineage — DAG back to the immutable root', () => {
|
|
it('a single gen-0 bundle reconstructs a trivially-intact, replayable lineage', () => {
|
|
const t = reconstructLineage([runSyntheticProofRound({ now: 1, generation: 0, parent: null })]);
|
|
expect(t.generations).toBe(1);
|
|
expect(t.promotions).toBe(1);
|
|
expect(t.lineageIntact).toBe(true);
|
|
expect(t.rootHash).toBeTruthy();
|
|
expect(t.branches).toEqual(['main']);
|
|
});
|
|
|
|
it('a chained lineage reconstructs back to the immutable root with intact DAG invariants', () => {
|
|
const t = reconstructLineage(chainOf(3));
|
|
expect(t.promotions).toBe(3);
|
|
expect(t.lineageIntact).toBe(true);
|
|
expect(t.cumulativeHeldOutImprovement).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('supports branches (DAG, not a linked list) — a fork off gen 0 keeps the lineage intact', () => {
|
|
const main = chainOf(2, 'main');
|
|
// branch off the root (main gen 0) into a separate branch that inherits its policy.
|
|
const rootCand = main[0].candidateManifest.policy.value as Record<string, number>;
|
|
const branch = runSyntheticProofRound({
|
|
now: 99, generation: 1, branch: 'legal', parent: main[0].candidateManifestHash,
|
|
baseline: rootCand, candidate: { ...rootCand, subjectWeight: 3 },
|
|
});
|
|
const t = reconstructLineage([...main, branch]);
|
|
expect(t.branches.sort()).toEqual(['legal', 'main']);
|
|
expect(t.lineageIntact).toBe(true);
|
|
});
|
|
|
|
it('detects >1 root, a missing parent, and a non-inheriting child', () => {
|
|
expect(reconstructLineage([runSyntheticProofRound({ now: 1, generation: 1, parent: 'sha256:ghost' })]).lineageIntact).toBe(false);
|
|
const g0 = runSyntheticProofRound({ now: 1, generation: 0, parent: null });
|
|
const g0b = runSyntheticProofRound({ now: 2, generation: 0, parent: null }); // 2nd root
|
|
expect(reconstructLineage([g0, g0b]).problems.some((p) => /one immutable root/.test(p))).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('mutationEffectiveness — evidence-grounded meta-learning', () => {
|
|
it('aggregates attempts/promotions/mean-delta per mutation class', () => {
|
|
const bundles = [
|
|
runSyntheticProofRound({ now: 1 }), // multi, promoted
|
|
runSyntheticProofRound({ now: 2, regress: true }), // multi, rejected
|
|
];
|
|
const stats = mutationEffectiveness(bundles);
|
|
const multi = stats.find((s) => s.mutationClass === 'retrieval:multi')!;
|
|
expect(multi.attempts).toBe(2);
|
|
expect(multi.promotions).toBe(1);
|
|
expect(multi.meanDelta).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('accept/v2+seq — sequential-evidence conjunct (ADR-381 §3)', () => {
|
|
const baseline = { alpha: 0.5, subjectWeight: 2, mmrLambda: 0.7, bodyWeight: 1, typePenaltyFactor: 1 };
|
|
const candidate = { alpha: 0.3, subjectWeight: 1, mmrLambda: 0.5, bodyWeight: 1.5, typePenaltyFactor: 0.5 };
|
|
// 10 all-win pairs: clears test 1 (1.5^10 ≈ 57.7 ≥ 32.9) but not test 2 (≈131.6).
|
|
const strongHoldout = Array.from({ length: 10 }, (_, i) => ({ taskId: `s${i}`, baselineScore: 0.5, candidateScore: 0.6 + i / 100 }));
|
|
// 5 all-win pairs: bootstrap-significant but sequentially insufficient at test 1.
|
|
const weakHoldout = Array.from({ length: 5 }, (_, i) => ({ taskId: `w${i}`, baselineScore: 0.5, candidateScore: 0.62 + i / 100 }));
|
|
|
|
it('promotes under v2 only when the sequential term also holds, and pins the v2 version', () => {
|
|
const b = runRealEvolveRound({ baseline, candidate, holdout: strongHoldout, generation: 0, parent: null, now: 1, corpus: 'x', sequential: { testIndex: 1 } });
|
|
expect(b.meetsPromotionRule.version).toBe('accept/v2+seq');
|
|
expect(b.sequentialEvidence).toMatchObject({ testIndex: 1, significant: true, informativePairs: 10 });
|
|
expect(b.decisionReceipt.promoted).toBe(true);
|
|
});
|
|
|
|
it('rejects a bootstrap-significant candidate whose sequential evidence is insufficient — cause "sequential"', () => {
|
|
const b = runRealEvolveRound({ baseline, candidate, holdout: weakHoldout, generation: 0, parent: null, now: 1, corpus: 'x', sequential: { testIndex: 1 } });
|
|
expect(b.decisionReceipt.significant).toBe(true); // v1 term holds
|
|
expect(b.sequentialEvidence?.significant).toBe(false); // v2 term refuses
|
|
expect(b.decisionReceipt.promoted).toBe(false);
|
|
expect(b.regression?.failureCause).toBe('sequential');
|
|
expect(b.decisionReceipt.reason).toMatch(/sequential_evidence/);
|
|
});
|
|
|
|
it('the same evidence fails at a later stream position (alpha allocation bites)', () => {
|
|
const early = runRealEvolveRound({ baseline, candidate, holdout: strongHoldout, generation: 0, parent: null, now: 1, corpus: 'x', sequential: { testIndex: 1 } });
|
|
const late = runRealEvolveRound({ baseline, candidate, holdout: strongHoldout, generation: 5, parent: null, now: 1, corpus: 'x', sequential: { testIndex: 2 } });
|
|
expect(early.decisionReceipt.promoted).toBe(true);
|
|
expect(late.decisionReceipt.promoted).toBe(false);
|
|
});
|
|
|
|
it('v2 bundles replay independently, and a tampered sequential record is detected', () => {
|
|
const b = runRealEvolveRound({ baseline, candidate, holdout: strongHoldout, generation: 0, parent: null, now: 1, corpus: 'x', sequential: { testIndex: 1 } });
|
|
expect(verifyReceiptBundle(b).valid).toBe(true);
|
|
expect(verifyReceiptBundle(b).explanation).toMatch(/accept\/v2\+seq/);
|
|
|
|
const tampered = structuredClone(b);
|
|
tampered.sequentialEvidence!.eValue = 999;
|
|
const v = verifyReceiptBundle(tampered);
|
|
expect(v.valid).toBe(false);
|
|
expect(v.mismatches.join(' ')).toMatch(/sequential-evidence verdict does not recompute/);
|
|
|
|
// Claiming an easier stream position than recorded is also caught: the
|
|
// verdict was recorded at index 2; rewriting it to index 1 changes the
|
|
// threshold, so significance no longer recomputes.
|
|
const rejected = runRealEvolveRound({ baseline, candidate, holdout: strongHoldout, generation: 5, parent: null, now: 1, corpus: 'x', sequential: { testIndex: 2 } });
|
|
const indexShopped = structuredClone(rejected);
|
|
indexShopped.sequentialEvidence!.testIndex = 1;
|
|
expect(verifyReceiptBundle(indexShopped).valid).toBe(false);
|
|
});
|
|
|
|
it('v1 bundles (no sequential input) keep v1 semantics and still verify', () => {
|
|
const b = runRealEvolveRound({ baseline, candidate, holdout: strongHoldout, generation: 0, parent: null, now: 1, corpus: 'x' });
|
|
expect(b.meetsPromotionRule.version).toBe(PROMOTION_RULE_VERSION);
|
|
expect(b.sequentialEvidence).toBeUndefined();
|
|
expect(verifyReceiptBundle(b).valid).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('detectPlateau — rigorous, not intuitive', () => {
|
|
it('insufficient-data below the window', () => {
|
|
expect(detectPlateau(chainOf(3), { window: 20 }).status).toBe('insufficient-data');
|
|
});
|
|
|
|
it('reports active while improvements keep landing', () => {
|
|
const r = detectPlateau(chainOf(20), { window: 20 });
|
|
expect(r.status).toBe('active');
|
|
expect(r.promotionRate).toBe(1);
|
|
});
|
|
});
|