From 6be26a2e1a7827243b8a90d2baab8f6ed69cc206 Mon Sep 17 00:00:00 2001 From: Giovanni Giovanni Date: Mon, 13 Jul 2026 15:11:13 -0400 Subject: [PATCH] feat(core): add pure loadUwrProfile validator (PR-UWR-RUNTIME-LOADER) Co-Authored-By: Claude Fable 5 --- validators/UniversalWeightingRule.ts | 6 +- validators/UwrProfileLoader.ts | 216 +++++++++ validators/__tests__/UwrProfileLoader.test.ts | 411 ++++++++++++++++++ 3 files changed, 632 insertions(+), 1 deletion(-) create mode 100644 validators/UwrProfileLoader.ts create mode 100644 validators/__tests__/UwrProfileLoader.test.ts diff --git a/validators/UniversalWeightingRule.ts b/validators/UniversalWeightingRule.ts index 4463cbc..7d5ff70 100644 --- a/validators/UniversalWeightingRule.ts +++ b/validators/UniversalWeightingRule.ts @@ -41,7 +41,11 @@ export interface UniversalWeightingRuleConfig { /** * Default UWR configuration placeholder. * - * TODO: Replace with governance-approved weights sourced from afi-config. + * TODO: Runtime replacement of this stub by the registered afi-config profile + * is governed by afi-governance decisions/uwr-runtime-consumption-v0.1.md. + * The pure loader/validator exists (PR-UWR-RUNTIME-LOADER, + * ./UwrProfileLoader.ts); the runtime registry read and any change to this + * constant remain separately authorized (PR-UWR-RUNTIME-READ, RC-8). */ export const defaultUwrConfig: Readonly = { id: "uwr-default-stub", diff --git a/validators/UwrProfileLoader.ts b/validators/UwrProfileLoader.ts new file mode 100644 index 0000000..16b444d --- /dev/null +++ b/validators/UwrProfileLoader.ts @@ -0,0 +1,216 @@ +/** + * PR-UWR-RUNTIME-LOADER: pure loader/validator for the registered UWR profile. + * + * Authorized by afi-governance `decisions/uwr-runtime-consumption-v0.1.md` + * (§7 row PR-UWR-RUNTIME-LOADER, flipped by owner merge of afi-governance + * PR #11 per RC-12). Scope per RC-2/RC-5: validate a passed-in, + * already-parsed registry document and map it onto + * {@link UniversalWeightingRuleConfig}. PURE by decision: no `fs`, no path + * resolution, no afi-config dependency — the caller supplies the parsed + * document. Nothing here reads the registry at runtime; the composition-root + * read is separately authorized (PR-UWR-RUNTIME-READ, RC-3/RC-4), and + * `defaultUwrConfig` remains the live config unchanged (RC-8). + */ + +import { + defaultUwrConfig, + type UniversalWeightingRuleConfig +} from "./UniversalWeightingRule.js"; + +/** Document-format id accepted by this loader (RC-2 "schema id"). */ +export const UWR_PROFILE_SCHEMA_ID = "afi.uwr-profile.v0"; + +/** + * The single registrable profile id (UP-2/UP-10 pin; RC-5 condition 3). + * Any other id — including `defaultUwrConfig.id` itself — is refused. + */ +export const PINNED_UWR_PROFILE_ID = "uwr-weighted-lifts-v0.1"; + +/** Axis registry, order significant (UP-4; RC-5 condition 2). */ +export const PINNED_UWR_AXES = Object.freeze([ + "structure", + "execution", + "risk", + "insight" +] as const); + +type WeightKey = Exclude; + +/** + * The weight keys a profile document must carry (RC-2 "weight shape"), + * derived from the pinned axes so the `axis → ${axis}Weight` correspondence + * is structural, and compile-checked against + * {@link UniversalWeightingRuleConfig} so a config-field rename cannot + * silently diverge from this list. + */ +const WEIGHT_KEYS: readonly WeightKey[] = Object.freeze( + PINNED_UWR_AXES.map((axis): WeightKey => `${axis}Weight`) +); + +/** + * Machine-checkable refusal reasons. Each maps to a violated condition of the + * RC-5 identity predicate (which RC-4 defines as the fail-closed mismatch + * trigger) or to a document-shape precondition of evaluating it. + */ +export type UwrProfileLoadErrorReason = + | "not-an-object" + | "schema-mismatch" + | "profile-id-mismatch" + | "supersedes-mismatch" + | "axes-mismatch" + | "weights-shape-mismatch" + | "weight-value-mismatch"; + +/** Refusal error thrown by {@link loadUwrProfile}; never swallowed here. */ +export class UwrProfileLoadError extends Error { + readonly reason: UwrProfileLoadErrorReason; + + constructor(reason: UwrProfileLoadErrorReason, detail: string) { + super(`UWR profile load refused (${reason}): ${detail}`); + this.name = "UwrProfileLoadError"; + this.reason = reason; + } +} + +function fail(reason: UwrProfileLoadErrorReason, detail: string): never { + throw new UwrProfileLoadError(reason, detail); +} + +/** + * Read an OWN property exactly once. Inherited (prototype-supplied) values + * must never satisfy the predicate, and accessor-backed documents must not + * get a second read after validation — every field below is read once into a + * local and only the local is used. + */ +function ownValue(record: Record, key: string): unknown { + return Object.prototype.hasOwnProperty.call(record, key) + ? record[key] + : undefined; +} + +/** + * Validate a parsed UWR profile registry document and map it onto + * {@link UniversalWeightingRuleConfig}. + * + * Enforces the RC-5 identity predicate against {@link defaultUwrConfig}: + * 1. the four weights equal `defaultUwrConfig`'s per axis; + * 2. the axes array equals the pinned registry in content and order; + * 3. `profileId` equals the pinned `uwr-weighted-lifts-v0.1` AND + * `supersedes` equals `defaultUwrConfig.id`; + * 4. id fields are deliberately NOT compared for direct equality — the ids + * differ by design (supersession, not equality, is the pinned relation). + * + * Fields this loader does not consume (engine, outputSurface, decaySurface, + * qualification, scorerIdentity, katRefs, doctrineRefs, …) are ignored, not + * validated: full document validation is owned by the afi-config schema and + * its CI pin guards. + * + * @param profileJson - Already-parsed registry document (caller does the I/O) + * @returns Frozen config whose weight values are `defaultUwrConfig`'s own + * (identity by construction — the predicate proved the document's + * values equal them, so registry-supplied numbers never flow into + * the result) and whose `id` is the registered profile id. The `id` + * records which governed profile the values were validated against; + * it does NOT signal that any registry was read at runtime — stamp + * and consumption semantics remain governed by RC-6/RC-8. + * @throws UwrProfileLoadError on any shape or predicate violation + */ +export function loadUwrProfile( + profileJson: unknown +): Readonly { + if ( + typeof profileJson !== "object" || + profileJson === null || + Array.isArray(profileJson) + ) { + fail("not-an-object", `expected a plain object, got ${describe(profileJson)}`); + } + const doc = profileJson as Record; + + const schema = ownValue(doc, "schema"); + if (schema !== UWR_PROFILE_SCHEMA_ID) { + fail( + "schema-mismatch", + `expected schema "${UWR_PROFILE_SCHEMA_ID}", got ${describe(schema)}` + ); + } + + // RC-5 condition 3 (first half): only the pinned profile id is loadable. + const profileId = ownValue(doc, "profileId"); + if (profileId !== PINNED_UWR_PROFILE_ID) { + fail( + "profile-id-mismatch", + `expected profileId "${PINNED_UWR_PROFILE_ID}", got ${describe(profileId)}` + ); + } + + // RC-5 condition 3 (second half): supersession is checked as data. + const supersedes = ownValue(doc, "supersedes"); + if (supersedes !== defaultUwrConfig.id) { + fail( + "supersedes-mismatch", + `expected supersedes "${defaultUwrConfig.id}", got ${describe(supersedes)}` + ); + } + + // RC-5 condition 2: axis registry equal in content and order. + const axes = ownValue(doc, "axes"); + if ( + !Array.isArray(axes) || + axes.length !== PINNED_UWR_AXES.length || + !PINNED_UWR_AXES.every((name, i) => axes[i] === name) + ) { + fail( + "axes-mismatch", + `expected axes [${PINNED_UWR_AXES.join(", ")}] in order, got ${describe(axes)}` + ); + } + + // RC-2 weight shape: exactly the four pinned keys as own enumerable + // properties, each read once; RC-5 condition 1: each value strictly equal + // to defaultUwrConfig's. + const weights = ownValue(doc, "weights"); + if (typeof weights !== "object" || weights === null || Array.isArray(weights)) { + fail("weights-shape-mismatch", `expected a weights object, got ${describe(weights)}`); + } + const weightRecord = weights as Record; + const presentKeys = Object.keys(weightRecord); + if ( + presentKeys.length !== WEIGHT_KEYS.length || + !WEIGHT_KEYS.every((key) => presentKeys.includes(key)) + ) { + fail( + "weights-shape-mismatch", + `expected exactly keys [${WEIGHT_KEYS.join(", ")}], got [${presentKeys.join(", ")}]` + ); + } + for (const key of WEIGHT_KEYS) { + const value = weightRecord[key]; + if (typeof value !== "number" || !Number.isFinite(value)) { + fail("weights-shape-mismatch", `${key} must be a finite number, got ${describe(value)}`); + } + if (value !== defaultUwrConfig[key]) { + fail( + "weight-value-mismatch", + `${key} must equal defaultUwrConfig.${key} (${defaultUwrConfig[key]}), got ${String(value)}` + ); + } + } + + // RC-5 condition 4 is enforced by omission: no profileId === defaultUwrConfig.id + // comparison exists anywhere above. + // + // Identity by construction: the predicate just proved the document's weight + // values equal defaultUwrConfig's, so the returned config spreads + // defaultUwrConfig itself — emitting registry-supplied numbers is + // structurally impossible, whatever future edits do to the checks above. + return Object.freeze({ ...defaultUwrConfig, id: PINNED_UWR_PROFILE_ID }); +} + +/** Compact value description for refusal messages (never throws). */ +function describe(value: unknown): string { + if (value === null) return "null"; + if (Array.isArray(value)) return `array(${value.length})`; + if (typeof value === "string") return `"${value}"`; + return `${typeof value}${typeof value === "number" ? ` ${String(value)}` : ""}`; +} diff --git a/validators/__tests__/UwrProfileLoader.test.ts b/validators/__tests__/UwrProfileLoader.test.ts new file mode 100644 index 0000000..b1071ac --- /dev/null +++ b/validators/__tests__/UwrProfileLoader.test.ts @@ -0,0 +1,411 @@ +/** + * PR-UWR-RUNTIME-LOADER: unit tests for the pure `loadUwrProfile` + * validator/mapper (afi-governance decisions/uwr-runtime-consumption-v0.1.md, + * RC-2/RC-5; §7 row flipped via afi-governance PR #11). + * + * The primary fixture is an inline object literal mirroring + * afi-config `registries/uwr-profiles/uwr-weighted-lifts-v0.1.json` + * @ merge fe329164919f0c1c9dc24bb5c279978fb680e983 — the unit tests perform + * no I/O, matching the loader's own purity. One dev-only integration test + * additionally reads the sibling afi-config checkout when present (skipped + * otherwise), following the established pattern of + * ./computeUwrScore.kat.test.ts. + * + * Scope: loading ≠ wiring — nothing here consumes the registry at runtime, + * `defaultUwrConfig` is unchanged, and no reward, mint, or validator-scoring + * path is touched. The runtime read is separately authorized + * (PR-UWR-RUNTIME-READ). + */ + +import { existsSync, readFileSync } from "node:fs"; +import { describe, it, expect } from "vitest"; +import { + loadUwrProfile, + UwrProfileLoadError, + PINNED_UWR_PROFILE_ID, + PINNED_UWR_AXES, + UWR_PROFILE_SCHEMA_ID, + type UwrProfileLoadErrorReason +} from "../UwrProfileLoader.js"; +import { + computeUwrScore, + defaultUwrConfig, + type UwrAxesInput +} from "../UniversalWeightingRule.js"; + +/** Sibling afi-config checkout (dev machines only; CI has no sibling). */ +const SIBLING_REGISTRY_URL = new URL( + "../../../afi-config/registries/uwr-profiles/uwr-weighted-lifts-v0.1.json", + import.meta.url +); + +/** + * Inline mirror of the registered profile document (fields the loader + * consumes are exact; consumed-irrelevant blocks are carried to prove they + * are ignored, abbreviated where their content cannot matter). + */ +function validRegistryDocument(): Record { + return { + schema: "afi.uwr-profile.v0", + "x-afiStatus": "draft-non-implementation", + profileId: "uwr-weighted-lifts-v0.1", + humanAlias: "Testnet Scoring Profile v0", + status: "testnet-provisional", + supersedes: "uwr-default-stub", + engine: { + function: "computeUwrScore", + model: "normalized-weighted-average-of-clamped-axes", + sourceModule: "afi-core/validators/UniversalWeightingRule.ts" + }, + axes: ["structure", "execution", "risk", "insight"], + weights: { + structureWeight: 0.25, + executionWeight: 0.25, + riskWeight: 0.25, + insightWeight: 0.25 + }, + outputSurface: { + uwrScoreRange: { min: 0, max: 1 }, + riskBucketTaxonomy: ["low", "medium", "high", "extreme"], + convictionRange: { min: 0, max: 1 } + }, + qualification: { + minDecayScoreThreshold: 0.5, + challengeWindowDurationHours: 24, + rule: "decayedScore >= minDecayScoreThreshold" + }, + scorerIdentity: { + analystId: "froggy", + strategyId: "trend_pullback_v1", + invokedAs: "scoreFroggyTrendPullbackFromEnriched" + }, + katRefs: { + computeUwrScore: "kats/uwr-profile/v0/compute-uwr-score.kat.json", + applyTimeDecay: "kats/uwr-profile/v0/apply-time-decay.kat.json" + }, + doctrineRefs: [ + "afi-governance/decisions/uwr-profile-pin-v0.1.md", + "afi-governance/decisions/math-authority-v0.1.md", + "afi-governance/decisions/mint-formula-bt-86b-alignment-v0.1.md" + ] + }; +} + +function expectRefusal( + document: unknown, + reason: UwrProfileLoadErrorReason +): void { + let caught: unknown; + try { + loadUwrProfile(document); + } catch (error) { + caught = error; + } + expect(caught, `expected a ${reason} refusal`).toBeInstanceOf( + UwrProfileLoadError + ); + expect((caught as UwrProfileLoadError).reason).toBe(reason); +} + +describe("PR-UWR-RUNTIME-LOADER: loadUwrProfile happy path", () => { + it("maps the registered document onto UniversalWeightingRuleConfig", () => { + const config = loadUwrProfile(validRegistryDocument()); + expect(config).toEqual({ + id: "uwr-weighted-lifts-v0.1", + structureWeight: 0.25, + executionWeight: 0.25, + riskWeight: 0.25, + insightWeight: 0.25 + }); + }); + + it("returns a frozen config and does not mutate the input", () => { + const document = validRegistryDocument(); + const snapshot = structuredClone(document); + const config = loadUwrProfile(document); + expect(Object.isFrozen(config)).toBe(true); + expect(document).toEqual(snapshot); + }); + + it("exposes the pinned constants it validates against", () => { + expect(PINNED_UWR_PROFILE_ID).toBe("uwr-weighted-lifts-v0.1"); + expect(UWR_PROFILE_SCHEMA_ID).toBe("afi.uwr-profile.v0"); + expect([...PINNED_UWR_AXES]).toEqual([ + "structure", + "execution", + "risk", + "insight" + ]); + }); +}); + +describe("PR-UWR-RUNTIME-LOADER: RC-5 identity with defaultUwrConfig", () => { + it("loaded weights are strictly equal to defaultUwrConfig's per axis (condition 1)", () => { + const config = loadUwrProfile(validRegistryDocument()); + expect(Object.is(config.structureWeight, defaultUwrConfig.structureWeight)).toBe(true); + expect(Object.is(config.executionWeight, defaultUwrConfig.executionWeight)).toBe(true); + expect(Object.is(config.riskWeight, defaultUwrConfig.riskWeight)).toBe(true); + expect(Object.is(config.insightWeight, defaultUwrConfig.insightWeight)).toBe(true); + }); + + it("ids relate by supersession, not equality (conditions 3 and 4)", () => { + const document = validRegistryDocument(); + const config = loadUwrProfile(document); + expect(config.id).not.toBe(defaultUwrConfig.id); + expect(document.supersedes).toBe(defaultUwrConfig.id); + }); + + it("a document carrying defaultUwrConfig's own id is refused (only the pin loads)", () => { + const document = validRegistryDocument(); + document.profileId = "uwr-default-stub"; + expectRefusal(document, "profile-id-mismatch"); + }); + + it("computeUwrScore is bit-identical under loaded vs default config (zero behavior change)", () => { + const config = loadUwrProfile(validRegistryDocument()); + const vectors: UwrAxesInput[] = [ + // D2 M2 golden anchor axes (UP-5): expected uwrScore 0.1875. + { structureAxis: 0.15, executionAxis: 0, riskAxis: 0.2, insightAxis: 0.4 }, + { structureAxis: 0.5, executionAxis: 0.5, riskAxis: 0.5, insightAxis: 0.5 }, + { structureAxis: 1, executionAxis: 1, riskAxis: 1, insightAxis: 1 }, + { structureAxis: 0, executionAxis: 0, riskAxis: 0, insightAxis: 0 }, + { structureAxis: 0.8, executionAxis: 0.7, riskAxis: 0.9, insightAxis: 0.9 } + ]; + for (const axes of vectors) { + expect( + Object.is(computeUwrScore(axes, config), computeUwrScore(axes, defaultUwrConfig)) + ).toBe(true); + } + // The absolute UP-5 golden anchor, asserted here on purpose even though + // ./computeUwrScore.kat.test.ts owns the anchor generally: RC-10 makes + // anchor stability an acceptance criterion for every program PR, so the + // loader suite braces it against a drifted defaultUwrConfig too. + expect( + computeUwrScore( + { structureAxis: 0.15, executionAxis: 0, riskAxis: 0.2, insightAxis: 0.4 }, + config + ) + ).toBe(0.1875); + }); +}); + +describe("PR-UWR-RUNTIME-LOADER: shape refusals", () => { + it("refuses non-object inputs", () => { + expectRefusal(null, "not-an-object"); + expectRefusal(undefined, "not-an-object"); + expectRefusal("uwr-weighted-lifts-v0.1", "not-an-object"); + expectRefusal(0.25, "not-an-object"); + expectRefusal([validRegistryDocument()], "not-an-object"); + }); + + it("refuses a wrong or missing schema id", () => { + const wrongSchema = validRegistryDocument(); + wrongSchema.schema = "afi.uwr-profile.v1"; + expectRefusal(wrongSchema, "schema-mismatch"); + + const missingSchema = validRegistryDocument(); + delete missingSchema.schema; + expectRefusal(missingSchema, "schema-mismatch"); + }); + + it("refuses a wrong, missing, or alias-as-id profileId", () => { + const wrongId = validRegistryDocument(); + wrongId.profileId = "uwr-weighted-lifts-v0.2"; + expectRefusal(wrongId, "profile-id-mismatch"); + + const aliasAsId = validRegistryDocument(); + aliasAsId.profileId = "Testnet Scoring Profile v0"; + expectRefusal(aliasAsId, "profile-id-mismatch"); + + const missingId = validRegistryDocument(); + delete missingId.profileId; + expectRefusal(missingId, "profile-id-mismatch"); + }); + + it("refuses a wrong or missing supersedes", () => { + const wrongSupersedes = validRegistryDocument(); + wrongSupersedes.supersedes = "uwr-default-stub-v2"; + expectRefusal(wrongSupersedes, "supersedes-mismatch"); + + const missingSupersedes = validRegistryDocument(); + delete missingSupersedes.supersedes; + expectRefusal(missingSupersedes, "supersedes-mismatch"); + }); + + it("refuses axes drift: reorder, drop, extend, rename, non-array", () => { + const reordered = validRegistryDocument(); + reordered.axes = ["execution", "structure", "risk", "insight"]; + expectRefusal(reordered, "axes-mismatch"); + + const dropped = validRegistryDocument(); + dropped.axes = ["structure", "execution", "risk"]; + expectRefusal(dropped, "axes-mismatch"); + + const extended = validRegistryDocument(); + extended.axes = ["structure", "execution", "risk", "insight", "novelty"]; + expectRefusal(extended, "axes-mismatch"); + + // Gateway-drift axis names (recorded non-conformant per UP-4). + const renamed = validRegistryDocument(); + renamed.axes = ["utility", "workQuality", "rarity", "insight"]; + expectRefusal(renamed, "axes-mismatch"); + + const nonArray = validRegistryDocument(); + nonArray.axes = "structure,execution,risk,insight"; + expectRefusal(nonArray, "axes-mismatch"); + }); + + it("refuses weights shape violations: missing, extra, non-numeric, NaN", () => { + const missingKey = validRegistryDocument(); + missingKey.weights = { + structureWeight: 0.25, + executionWeight: 0.25, + riskWeight: 0.25 + }; + expectRefusal(missingKey, "weights-shape-mismatch"); + + const extraKey = validRegistryDocument(); + extraKey.weights = { + structureWeight: 0.25, + executionWeight: 0.25, + riskWeight: 0.25, + insightWeight: 0.25, + noveltyWeight: 0 + }; + expectRefusal(extraKey, "weights-shape-mismatch"); + + const stringWeight = validRegistryDocument(); + stringWeight.weights = { + structureWeight: "0.25", + executionWeight: 0.25, + riskWeight: 0.25, + insightWeight: 0.25 + }; + expectRefusal(stringWeight, "weights-shape-mismatch"); + + const nanWeight = validRegistryDocument(); + nanWeight.weights = { + structureWeight: Number.NaN, + executionWeight: 0.25, + riskWeight: 0.25, + insightWeight: 0.25 + }; + expectRefusal(nanWeight, "weights-shape-mismatch"); + + const nonObject = validRegistryDocument(); + nonObject.weights = [0.25, 0.25, 0.25, 0.25]; + expectRefusal(nonObject, "weights-shape-mismatch"); + }); + + it("refuses weight value drift, including near-misses (RC-5 condition 1)", () => { + const nearMissLow = validRegistryDocument(); + nearMissLow.weights = { + structureWeight: 0.2499, + executionWeight: 0.25, + riskWeight: 0.25, + insightWeight: 0.25 + }; + expectRefusal(nearMissLow, "weight-value-mismatch"); + + // One ULP above 0.25 — the smallest representable drift. + const nearMissUlp = validRegistryDocument(); + nearMissUlp.weights = { + structureWeight: 0.25 + 2 ** -54, + executionWeight: 0.25, + riskWeight: 0.25, + insightWeight: 0.25 + }; + expectRefusal(nearMissUlp, "weight-value-mismatch"); + + const negated = validRegistryDocument(); + negated.weights = { + structureWeight: -0.25, + executionWeight: 0.25, + riskWeight: 0.25, + insightWeight: 0.25 + }; + expectRefusal(negated, "weight-value-mismatch"); + }); + + it("ignores fields the loader does not consume", () => { + const document = validRegistryDocument(); + document.decaySurface = { family: "GreeksDecayTemplate", version: "v1" }; + document["x-futureField"] = { anything: true }; + expect(loadUwrProfile(document).id).toBe("uwr-weighted-lifts-v0.1"); + }); +}); + +describe("PR-UWR-RUNTIME-LOADER: hostile-input hardening (fail-closed)", () => { + it("reads each weight exactly once, so accessor-backed values cannot change after validation", () => { + let reads = 0; + const document = validRegistryDocument(); + document.weights = { + get structureWeight() { + reads += 1; + return reads === 1 ? 0.25 : 9; + }, + executionWeight: 0.25, + riskWeight: 0.25, + insightWeight: 0.25 + }; + const config = loadUwrProfile(document); + expect(reads).toBe(1); + expect(Object.is(config.structureWeight, 0.25)).toBe(true); + }); + + it("refuses an accessor that lies on its first (only) read", () => { + const document = validRegistryDocument(); + document.weights = { + get structureWeight() { + return 9; + }, + executionWeight: 0.25, + riskWeight: 0.25, + insightWeight: 0.25 + }; + expectRefusal(document, "weight-value-mismatch"); + }); + + it("inherited (prototype-supplied) fields never satisfy the predicate", () => { + // A document with NO own properties, everything inherited from a + // fully-conforming prototype: must be refused at the first own-field check. + const ghost = Object.create(validRegistryDocument()); + expectRefusal(ghost, "schema-mismatch"); + + // Same for the weights object specifically: inherited weight keys are + // not own keys, so the shape check refuses. + const document = validRegistryDocument(); + document.weights = Object.create({ + structureWeight: 0.25, + executionWeight: 0.25, + riskWeight: 0.25, + insightWeight: 0.25 + }); + expectRefusal(document, "weights-shape-mismatch"); + }); + + it("returned weight values are defaultUwrConfig's own (identity by construction)", () => { + const config = loadUwrProfile(validRegistryDocument()); + expect(config.structureWeight).toBe(defaultUwrConfig.structureWeight); + expect(config.executionWeight).toBe(defaultUwrConfig.executionWeight); + expect(config.riskWeight).toBe(defaultUwrConfig.riskWeight); + expect(config.insightWeight).toBe(defaultUwrConfig.insightWeight); + }); +}); + +describe("PR-UWR-RUNTIME-LOADER: sibling registry integration (dev-only)", () => { + it.skipIf(!existsSync(SIBLING_REGISTRY_URL))( + "the live sibling afi-config registry document loads through the RC-5 predicate", + () => { + const sibling = JSON.parse(readFileSync(SIBLING_REGISTRY_URL, "utf8")); + const config = loadUwrProfile(sibling); + expect(config).toEqual({ + id: PINNED_UWR_PROFILE_ID, + structureWeight: 0.25, + executionWeight: 0.25, + riskWeight: 0.25, + insightWeight: 0.25 + }); + } + ); +});