mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-11-05 04:51:00 +00:00
feat(core): rebuild governance core to the plan's decision model
The evaluator was a skeleton with invented thresholds (5/3/10/5) and two unimplemented branches. Rebuild it against the migration plan's spec and the behavior characterized from the pinned reference commit. Core: - four independent decision dimensions (ranking, visibility, interaction, transaction) with severity ladders and max-merge composition - stable reason identifiers replacing free-text strings - structured evidence, redacted unless a profile opts in - administrative-state reduction with capability filtering at merge time, so revoking a role drops that actor's entries immediately - protected actors stripped from every denial set - snapshot fingerprints for cache invalidation - generic policy engine: applications supply profiles and category thresholds; the core carries none of its own Characterization: - 34 generated fixtures covering every behavior the plan's Phase 0 requires - conformance suite that actually evaluates them, replacing a placeholder that asserted a list's length - reference thresholds (3/3/20/5, 60-day mute window) live in bitvid-compat as an application profile, not as engine defaults Fixes found while building: - reduceAdminState accepted malformed target identifiers, because getTargetKey formats anything it is given - community-user-deny fired for any contributor rather than only for community-curated sources 273 tests pass; typecheck and the reference-import guard are clean.
This commit is contained in:
1 parent
5a7c1a26de
commit
4f689184e2
60 files changed
+7870
-2161
No files matched your search
@@ -1,24 +1,197 @@
|
||||
// Shared test utilities for conformance harness
|
||||
export function createFixture(name, state, target, expectation) {
|
||||
return {
|
||||
name,
|
||||
state: {
|
||||
adminState: state.adminState || {},
|
||||
trust: state.trust || {},
|
||||
reports: state.reports || [],
|
||||
trustedMutes: state.trustedMutes || [],
|
||||
overrides: state.overrides || {},
|
||||
// Conformance harness for @nostr-governance
|
||||
//
|
||||
// Turns a JSON fixture into a snapshot, evaluates it, and compares the decision
|
||||
// against the fixture's expectation. Expectations are partial by design: a
|
||||
// fixture asserts the dimensions it characterizes and stays silent about the
|
||||
// rest, so adding a new decision field does not require touching every case.
|
||||
|
||||
import {
|
||||
createAuthorityState,
|
||||
createSnapshot,
|
||||
createViewerState,
|
||||
evaluateTarget,
|
||||
reduceAdminState,
|
||||
} from "@nostr-governance/core";
|
||||
|
||||
/**
|
||||
* @typedef {Object} FixtureExpectation
|
||||
* @property {{ effect: string, weight?: number }} [ranking]
|
||||
* @property {{ effect: string, overridable?: boolean }} [visibility]
|
||||
* @property {{ effect: string }} [interaction]
|
||||
* @property {{ effect: string }} [transaction]
|
||||
* @property {string[]} [reasons] - Exact set of reason ids, order-insensitive
|
||||
* @property {Record<string, number|boolean>} [evidence] - Subset of evidence fields
|
||||
*/
|
||||
|
||||
/**
|
||||
* Build a governance snapshot from a fixture.
|
||||
* @param {Object} fixture
|
||||
* @returns {{ snapshot: ReturnType<typeof createSnapshot>, viewer: ReturnType<typeof createViewerState> }}
|
||||
*/
|
||||
export function buildSnapshot(fixture) {
|
||||
const authority = createAuthorityState({
|
||||
root: fixture.authority?.root,
|
||||
actors: fixture.authority?.actors,
|
||||
protectedActors: fixture.authority?.protectedActors,
|
||||
});
|
||||
|
||||
const admin = reduceAdminState(fixture.contributions ?? [], authority);
|
||||
|
||||
/** @type {Map<string, Array<{ reporter: string, category: string, createdAt?: number }>>} */
|
||||
const reports = new Map();
|
||||
for (const [key, records] of Object.entries(fixture.reports ?? {})) {
|
||||
reports.set(key, records);
|
||||
}
|
||||
|
||||
/** @type {Map<string, Array<{ muter: string, category?: string, updatedAt?: number }>>} */
|
||||
const trustedMutes = new Map();
|
||||
for (const [key, records] of Object.entries(fixture.trustedMutes ?? {})) {
|
||||
trustedMutes.set(key, records);
|
||||
}
|
||||
|
||||
const snapshot = createSnapshot({
|
||||
authority,
|
||||
admin,
|
||||
trust: {
|
||||
contacts: new Set(fixture.trust?.contacts ?? []),
|
||||
seeds: new Set(fixture.trust?.seeds ?? []),
|
||||
},
|
||||
target,
|
||||
expectation,
|
||||
};
|
||||
reports,
|
||||
trustedMutes,
|
||||
});
|
||||
|
||||
/** @type {Map<string, { visibility: string, reason?: string }>} */
|
||||
const overrides = new Map();
|
||||
for (const entry of fixture.viewer?.overrides ?? []) {
|
||||
overrides.set(entry.key, { visibility: entry.visibility, reason: entry.reason });
|
||||
}
|
||||
|
||||
const viewer = createViewerState({
|
||||
blocks: new Set(fixture.viewer?.blocks ?? []),
|
||||
mutes: new Set(fixture.viewer?.mutes ?? []),
|
||||
overrides,
|
||||
});
|
||||
|
||||
return { snapshot, viewer };
|
||||
}
|
||||
|
||||
export function runConformanceTest(fixture, evaluator) {
|
||||
const decision = evaluator(fixture.state, fixture.target);
|
||||
return {
|
||||
passed: JSON.stringify(decision) === JSON.stringify(fixture.expectation),
|
||||
actual: decision,
|
||||
expected: fixture.expectation,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Evaluate a fixture against a policy definition.
|
||||
* @param {Object} fixture
|
||||
* @param {import('@nostr-governance/core').PolicyDefinition} policy
|
||||
* @returns {import('@nostr-governance/core').GovernanceDecision}
|
||||
*/
|
||||
export function evaluateFixture(fixture, policy) {
|
||||
const { snapshot, viewer } = buildSnapshot(fixture);
|
||||
return evaluateTarget(
|
||||
fixture.target,
|
||||
snapshot,
|
||||
{ surface: fixture.profile, policyProfile: fixture.profile, policy, now: fixture.now },
|
||||
viewer,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare a decision against a fixture expectation.
|
||||
* @param {import('@nostr-governance/core').GovernanceDecision} decision
|
||||
* @param {FixtureExpectation} expectation
|
||||
* @returns {string[]} Human-readable mismatches; empty when the fixture passes
|
||||
*/
|
||||
export function diffExpectation(decision, expectation) {
|
||||
/** @type {string[]} */
|
||||
const mismatches = [];
|
||||
|
||||
if (expectation.ranking) {
|
||||
if (decision.ranking.effect !== expectation.ranking.effect) {
|
||||
mismatches.push(
|
||||
`ranking.effect: expected "${expectation.ranking.effect}", got "${decision.ranking.effect}"`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
expectation.ranking.weight !== undefined &&
|
||||
decision.ranking.weight !== expectation.ranking.weight
|
||||
) {
|
||||
mismatches.push(
|
||||
`ranking.weight: expected ${expectation.ranking.weight}, got ${decision.ranking.weight}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (expectation.visibility) {
|
||||
if (decision.visibility.effect !== expectation.visibility.effect) {
|
||||
mismatches.push(
|
||||
`visibility.effect: expected "${expectation.visibility.effect}", got "${decision.visibility.effect}"`,
|
||||
);
|
||||
}
|
||||
if (
|
||||
expectation.visibility.overridable !== undefined &&
|
||||
decision.visibility.overridable !== expectation.visibility.overridable
|
||||
) {
|
||||
mismatches.push(
|
||||
`visibility.overridable: expected ${expectation.visibility.overridable}, got ${decision.visibility.overridable}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (expectation.interaction && decision.interaction.effect !== expectation.interaction.effect) {
|
||||
mismatches.push(
|
||||
`interaction.effect: expected "${expectation.interaction.effect}", got "${decision.interaction.effect}"`,
|
||||
);
|
||||
}
|
||||
|
||||
if (expectation.transaction) {
|
||||
const actual = decision.transaction?.effect ?? "(absent)";
|
||||
if (actual !== expectation.transaction.effect) {
|
||||
mismatches.push(
|
||||
`transaction.effect: expected "${expectation.transaction.effect}", got "${actual}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (expectation.reasons) {
|
||||
const actual = [...new Set(decision.reasons.map((reason) => reason.id))].sort();
|
||||
const expected = [...new Set(expectation.reasons)].sort();
|
||||
if (actual.join(",") !== expected.join(",")) {
|
||||
mismatches.push(`reasons: expected [${expected.join(", ")}], got [${actual.join(", ")}]`);
|
||||
}
|
||||
}
|
||||
|
||||
if (expectation.evidence) {
|
||||
const evidence = decision.evidence;
|
||||
if (!evidence) {
|
||||
mismatches.push("evidence: expected evidence on the decision, got none");
|
||||
} else {
|
||||
for (const [field, expected] of Object.entries(expectation.evidence)) {
|
||||
// Pubkey lists are asserted by length: identity is covered elsewhere and
|
||||
// spelling out 20 keys per fixture would obscure what the case is about.
|
||||
if (field.endsWith("Count")) {
|
||||
const listField = field.slice(0, -"Count".length);
|
||||
const actual = /** @type {Record<string, unknown>} */ (evidence)[listField];
|
||||
const length = Array.isArray(actual) ? actual.length : -1;
|
||||
if (length !== expected) {
|
||||
mismatches.push(`evidence.${listField}.length: expected ${expected}, got ${length}`);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
const actual = /** @type {Record<string, unknown>} */ (evidence)[field];
|
||||
if (actual !== expected) {
|
||||
mismatches.push(`evidence.${field}: expected ${expected}, got ${actual}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mismatches;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run one fixture end to end.
|
||||
* @param {Object} fixture
|
||||
* @param {import('@nostr-governance/core').PolicyDefinition} policy
|
||||
* @returns {{ passed: boolean, mismatches: string[], decision: import('@nostr-governance/core').GovernanceDecision }}
|
||||
*/
|
||||
export function runConformanceCase(fixture, policy) {
|
||||
const decision = evaluateFixture(fixture, policy);
|
||||
const mismatches = diffExpectation(decision, fixture.expect ?? {});
|
||||
return { passed: mismatches.length === 0, mismatches, decision };
|
||||
}
|
||||
Reference in new issue
Block a user