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:
thePR0M3TH3AN committed 2026-08-09 17:33:41 -04:00
1 parent 5a7c1a26de
commit 4f689184e2
60 files changed
+7731 -2022

No files matched your search

@@ -0,0 +1,63 @@
{
"name": "admin-address-deny",
"description": "Denying an address hides it across revisions.",
"behavior": "Address denial is keyed by coordinate, so republishing does not clear it.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"kind": "address-deny",
"targets": [
{
"type": "address",
"kind": "30023",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4",
"identifier": "listing-1"
}
]
}
],
"target": {
"type": "address",
"kind": "30023",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4",
"identifier": "listing-1"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "hide",
"overridable": true
},
"interaction": {
"effect": "deny"
},
"reasons": [
"admin-address-deny"
],
"evidence": {
"addressDenied": true
}
}
}
@@ -0,0 +1,61 @@
{
"name": "admin-event-deny-applies",
"description": "The denied event itself is hidden.",
"behavior": "Exact-event denial hides the named event.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"kind": "event-deny",
"targets": [
{
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
}
]
}
],
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "hide",
"overridable": true
},
"interaction": {
"effect": "deny"
},
"reasons": [
"admin-event-deny"
],
"evidence": {
"eventDenied": true
}
}
}
@@ -0,0 +1,56 @@
{
"name": "admin-event-deny",
"description": "An exact event denial hides only that event.",
"behavior": "Event denial applies to the named event and does not travel to the author.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"kind": "event-deny",
"targets": [
{
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
}
]
}
],
"target": {
"type": "event",
"id": "2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c2c",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"reasons": []
}
}
@@ -0,0 +1,59 @@
{
"name": "admin-user-deny",
"description": "A moderator's user denial hides the creator.",
"behavior": "Administrative denial applies the profile's administrativeDeny effects.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"kind": "user-deny",
"targets": [
{
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
}
]
}
],
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "hide",
"overridable": true
},
"interaction": {
"effect": "deny"
},
"reasons": [
"admin-user-deny"
],
"evidence": {
"userDenied": true
}
}
}
@@ -0,0 +1,60 @@
{
"name": "author-deny-reaches-events",
"description": "Denying a creator hides that creator's events.",
"behavior": "Author-level denial reaches the author's content.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"kind": "user-deny",
"targets": [
{
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
}
]
}
],
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "hide",
"overridable": true
},
"interaction": {
"effect": "deny"
},
"reasons": [
"admin-user-deny"
],
"evidence": {
"userDenied": true
}
}
}
@@ -0,0 +1,65 @@
{
"name": "author-override",
"description": "An override on the author does not leak to the author's events.",
"behavior": "Overrides are keyed by exact target.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"kind": "user-deny",
"targets": [
{
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
}
]
}
],
"viewer": {
"overrides": [
{
"key": "user:d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4",
"visibility": "allow"
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "hide",
"overridable": true
},
"interaction": {
"effect": "deny"
},
"reasons": [
"admin-user-deny"
]
}
}
@@ -0,0 +1,98 @@
{
"name": "blocked-reporter-ignored",
"description": "Reports from accounts the viewer blocks do not count.",
"behavior": "Blocked reporters are skipped before aggregation.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"viewer": {
"blocks": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202"
]
},
"reports": {
"event:1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b": [
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0202020202020202020202020202020202020202020202020202020202020202",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0303030303030303030303030303030303030303030303030303030303030303",
"category": "nudity",
"createdAt": 1749913600
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"evidence": {
"trustedReportTotal": 1
}
}
}
@@ -0,0 +1,57 @@
{
"name": "cache-fallback-effective",
"description": "State reduced from a cached snapshot decides identically to live state.",
"behavior": "Administrative state stays effective when relays are unreachable.",
"profile": "feed",
"now": 1750000000,
"fromCache": true,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"kind": "user-deny",
"targets": [
{
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
}
]
}
],
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "hide",
"overridable": true
},
"interaction": {
"effect": "deny"
},
"reasons": [
"admin-user-deny"
]
}
}
@@ -0,0 +1,71 @@
{
"name": "clean-target-allowed",
"description": "A target with no signals against it is fully allowed.",
"behavior": "Governance is opt-in per signal; absence of evidence is not suspicion.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"reasons": []
}
}
@@ -0,0 +1,58 @@
{
"name": "community-blacklist-merge",
"description": "A curator's denial is honored without moderator authority.",
"behavior": "Curators hold contribute-user-deny only; their entries merge as a union.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3",
"kind": "user-deny",
"source": "community-list-1",
"targets": [
{
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
}
]
}
],
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "hide",
"overridable": true
},
"interaction": {
"effect": "deny"
},
"reasons": [
"admin-user-deny",
"community-user-deny"
]
}
}
@@ -0,0 +1,56 @@
{
"name": "curator-cannot-deny-events",
"description": "A curator's event denial is ignored.",
"behavior": "Contributions beyond an actor's capabilities are dropped at merge time.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3",
"kind": "event-deny",
"targets": [
{
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
}
]
}
],
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"reasons": []
}
}
@@ -0,0 +1,108 @@
{
"name": "denied-reporter-ignored",
"description": "Reports from administratively denied accounts do not count.",
"behavior": "Denied accounts are excluded from aggregation, so a denied account cannot manufacture reports.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"contributions": [
{
"actor": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"kind": "user-deny",
"targets": [
{
"type": "user",
"pubkey": "0101010101010101010101010101010101010101010101010101010101010101"
},
{
"type": "user",
"pubkey": "0202020202020202020202020202020202020202020202020202020202020202"
}
]
}
],
"reports": {
"event:1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b": [
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0202020202020202020202020202020202020202020202020202020202020202",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0303030303030303030303030303030303030303030303030303030303030303",
"category": "nudity",
"createdAt": 1749913600
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"evidence": {
"trustedReportTotal": 1
}
}
}
@@ -0,0 +1,88 @@
{
"name": "duplicate-muter-dedupe",
"description": "The same account muting repeatedly counts once.",
"behavior": "Muters are counted uniquely.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"trustedMutes": {
"user:d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4": [
{
"muter": "0101010101010101010101010101010101010101010101010101010101010101",
"updatedAt": 1749913600
},
{
"muter": "0101010101010101010101010101010101010101010101010101010101010101",
"updatedAt": 1749827200
},
{
"muter": "0202020202020202020202020202020202020202020202020202020202020202",
"updatedAt": 1749913600
}
]
},
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "downrank",
"weight": 1
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"evidence": {
"trustedMuteTotal": 2
}
}
}
@@ -0,0 +1,100 @@
{
"name": "duplicate-reporter-dedupe",
"description": "One account reporting the same category repeatedly counts once.",
"behavior": "Reports are deduplicated per reporter and category before counting.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"reports": {
"event:1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b": [
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "spam",
"createdAt": 1749913600
},
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "spam",
"createdAt": 1749827200
},
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "spam",
"createdAt": 1749740800
},
{
"reporter": "0202020202020202020202020202020202020202020202020202020202020202",
"category": "spam",
"createdAt": 1749913600
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"reasons": [
"trusted-report"
],
"evidence": {
"trustedReportTotal": 2
}
}
}
@@ -0,0 +1,93 @@
{
"name": "evidence-redacted-by-default",
"description": "Feed surfaces report counts but not who produced them.",
"behavior": "Reporter pubkeys are withheld unless the profile opts in.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"reports": {
"event:1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b": [
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0202020202020202020202020202020202020202020202020202020202020202",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0303030303030303030303030303030303030303030303030303030303030303",
"category": "nudity",
"createdAt": 1749913600
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "restrict",
"overridable": true
},
"interaction": {
"effect": "require-explicit-action"
},
"evidence": {
"trustedReportTotal": 3,
"trustedReporterPubkeysCount": 0
}
}
}
@@ -0,0 +1,164 @@
{
"name": "expired-trusted-mute",
"description": "Mutes older than the 60-day window are ignored.",
"behavior": "Stale mute lists must not hold a target down indefinitely.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"trustedMutes": {
"user:d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4": [
{
"muter": "0101010101010101010101010101010101010101010101010101010101010101",
"updatedAt": 1744729600
},
{
"muter": "0202020202020202020202020202020202020202020202020202020202020202",
"updatedAt": 1744729600
},
{
"muter": "0303030303030303030303030303030303030303030303030303030303030303",
"updatedAt": 1744729600
},
{
"muter": "0404040404040404040404040404040404040404040404040404040404040404",
"updatedAt": 1744729600
},
{
"muter": "0505050505050505050505050505050505050505050505050505050505050505",
"updatedAt": 1744729600
},
{
"muter": "0606060606060606060606060606060606060606060606060606060606060606",
"updatedAt": 1744729600
},
{
"muter": "0707070707070707070707070707070707070707070707070707070707070707",
"updatedAt": 1744729600
},
{
"muter": "0808080808080808080808080808080808080808080808080808080808080808",
"updatedAt": 1744729600
},
{
"muter": "0909090909090909090909090909090909090909090909090909090909090909",
"updatedAt": 1744729600
},
{
"muter": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"updatedAt": 1744729600
},
{
"muter": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"updatedAt": 1744729600
},
{
"muter": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"updatedAt": 1744729600
},
{
"muter": "0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"updatedAt": 1744729600
},
{
"muter": "0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"updatedAt": 1744729600
},
{
"muter": "0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"updatedAt": 1744729600
},
{
"muter": "1010101010101010101010101010101010101010101010101010101010101010",
"updatedAt": 1744729600
},
{
"muter": "1111111111111111111111111111111111111111111111111111111111111111",
"updatedAt": 1744729600
},
{
"muter": "1212121212121212121212121212121212121212121212121212121212121212",
"updatedAt": 1744729600
},
{
"muter": "1313131313131313131313131313131313131313131313131313131313131313",
"updatedAt": 1744729600
},
{
"muter": "1414141414141414141414141414141414141414141414141414141414141414",
"updatedAt": 1744729600
},
{
"muter": "1515151515151515151515151515151515151515151515151515151515151515",
"updatedAt": 1749913600
},
{
"muter": "1616161616161616161616161616161616161616161616161616161616161616",
"updatedAt": 1749913600
}
]
},
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "downrank",
"weight": 1
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"evidence": {
"trustedMuteTotal": 2
}
}
}
+158
View File
@@ -0,0 +1,158 @@
{
"name": "home-hide-bypass",
"description": "The home surface downranks instead of hiding.",
"behavior": "Home and Recent decline to hard-hide so one stale list cannot empty the feed.",
"profile": "home",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"trustedMutes": {
"user:d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4": [
{
"muter": "0101010101010101010101010101010101010101010101010101010101010101",
"updatedAt": 1749913600
},
{
"muter": "0202020202020202020202020202020202020202020202020202020202020202",
"updatedAt": 1749913600
},
{
"muter": "0303030303030303030303030303030303030303030303030303030303030303",
"updatedAt": 1749913600
},
{
"muter": "0404040404040404040404040404040404040404040404040404040404040404",
"updatedAt": 1749913600
},
{
"muter": "0505050505050505050505050505050505050505050505050505050505050505",
"updatedAt": 1749913600
},
{
"muter": "0606060606060606060606060606060606060606060606060606060606060606",
"updatedAt": 1749913600
},
{
"muter": "0707070707070707070707070707070707070707070707070707070707070707",
"updatedAt": 1749913600
},
{
"muter": "0808080808080808080808080808080808080808080808080808080808080808",
"updatedAt": 1749913600
},
{
"muter": "0909090909090909090909090909090909090909090909090909090909090909",
"updatedAt": 1749913600
},
{
"muter": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"updatedAt": 1749913600
},
{
"muter": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"updatedAt": 1749913600
},
{
"muter": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"updatedAt": 1749913600
},
{
"muter": "0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"updatedAt": 1749913600
},
{
"muter": "0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"updatedAt": 1749913600
},
{
"muter": "0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"updatedAt": 1749913600
},
{
"muter": "1010101010101010101010101010101010101010101010101010101010101010",
"updatedAt": 1749913600
},
{
"muter": "1111111111111111111111111111111111111111111111111111111111111111",
"updatedAt": 1749913600
},
{
"muter": "1212121212121212121212121212121212121212121212121212121212121212",
"updatedAt": 1749913600
},
{
"muter": "1313131313131313131313131313131313131313131313131313131313131313",
"updatedAt": 1749913600
},
{
"muter": "1414141414141414141414141414141414141414141414141414141414141414",
"updatedAt": 1749913600
}
]
},
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "downrank",
"weight": 1
},
"visibility": {
"effect": "restrict",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"reasons": [
"trusted-mute",
"trusted-mute-threshold",
"surface-policy-bypass"
]
}
}
@@ -0,0 +1,72 @@
{
"name": "moderator-trust-seed",
"description": "An anonymous viewer inherits trust seeds.",
"behavior": "Seeds are the fallback trust graph when the viewer follows nobody.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [],
"seeds": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303"
]
},
"reports": {
"event:1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b": [
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0202020202020202020202020202020202020202020202020202020202020202",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0303030303030303030303030303030303030303030303030303030303030303",
"category": "nudity",
"createdAt": 1749913600
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "restrict",
"overridable": true
},
"interaction": {
"effect": "require-explicit-action"
},
"evidence": {
"trustedReportTotal": 3
}
}
}
@@ -0,0 +1,114 @@
{
"name": "non-spam-reports-never-hide",
"description": "Eight trusted reports in a non-spam category restrict but never hide.",
"behavior": "Only spam has a report-driven hide threshold.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"reports": {
"event:1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b": [
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "misleading",
"createdAt": 1749913600
},
{
"reporter": "0202020202020202020202020202020202020202020202020202020202020202",
"category": "misleading",
"createdAt": 1749913600
},
{
"reporter": "0303030303030303030303030303030303030303030303030303030303030303",
"category": "misleading",
"createdAt": 1749913600
},
{
"reporter": "0404040404040404040404040404040404040404040404040404040404040404",
"category": "misleading",
"createdAt": 1749913600
},
{
"reporter": "0505050505050505050505050505050505050505050505050505050505050505",
"category": "misleading",
"createdAt": 1749913600
},
{
"reporter": "0606060606060606060606060606060606060606060606060606060606060606",
"category": "misleading",
"createdAt": 1749913600
},
{
"reporter": "0707070707070707070707070707070707070707070707070707070707070707",
"category": "misleading",
"createdAt": 1749913600
},
{
"reporter": "0808080808080808080808080808080808080808080808080808080808080808",
"category": "misleading",
"createdAt": 1749913600
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "restrict",
"overridable": true
},
"interaction": {
"effect": "require-explicit-action"
}
}
}
@@ -0,0 +1,80 @@
{
"name": "personal-block-precedence",
"description": "A viewer's own block hides the target regardless of other state.",
"behavior": "Viewer blocks are viewer-local and always enforced.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"viewer": {
"blocks": [
"d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
]
},
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "hide",
"overridable": true
},
"interaction": {
"effect": "deny"
},
"reasons": [
"viewer-block"
],
"evidence": {
"personalBlock": true
}
}
}
@@ -0,0 +1,56 @@
{
"name": "playback-enforces-hide",
"description": "Playback enforces the full decision with no surface exception.",
"behavior": "The hide bypass is a discovery-surface concession, not a global one.",
"profile": "playback",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"kind": "user-deny",
"targets": [
{
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
}
]
}
],
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "deny",
"overridable": true
},
"interaction": {
"effect": "deny"
},
"reasons": [
"admin-user-deny"
]
}
}
@@ -0,0 +1,93 @@
{
"name": "playback-exposes-evidence",
"description": "The playback surface exposes reporter pubkeys; discovery surfaces do not.",
"behavior": "exposeEvidence controls whether contributor pubkeys reach the consumer.",
"profile": "playback",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"reports": {
"event:1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b": [
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0202020202020202020202020202020202020202020202020202020202020202",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0303030303030303030303030303030303030303030303030303030303030303",
"category": "nudity",
"createdAt": 1749913600
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "restrict",
"overridable": true
},
"interaction": {
"effect": "require-explicit-action"
},
"evidence": {
"trustedReportTotal": 3,
"trustedReporterPubkeysCount": 3
}
}
}
@@ -0,0 +1,56 @@
{
"name": "protected-target",
"description": "The root administrator cannot be denied by contributor lists.",
"behavior": "Protected actors are stripped from every denial set.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"kind": "user-deny",
"targets": [
{
"type": "user",
"pubkey": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
}
]
}
],
"target": {
"type": "user",
"pubkey": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"evidence": {
"protectedTarget": true
}
}
}
@@ -0,0 +1,57 @@
{
"name": "recent-hide-bypass",
"description": "The recent surface applies the same exception as home.",
"behavior": "Recent shares the home surface exception.",
"profile": "recent",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"kind": "user-deny",
"targets": [
{
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
}
]
}
],
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "restrict",
"overridable": true
},
"interaction": {
"effect": "deny"
},
"reasons": [
"admin-user-deny",
"surface-policy-bypass"
]
}
}
@@ -0,0 +1,48 @@
{
"name": "revoked-actor-contribution-dropped",
"description": "An actor with no roles cannot deny anyone.",
"behavior": "Revocation takes effect immediately because capabilities resolve at merge time.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"contributions": [
{
"actor": "b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2",
"kind": "user-deny",
"targets": [
{
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
}
]
}
],
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"reasons": []
}
}
@@ -0,0 +1,74 @@
{
"name": "seeds-yield-to-real-contacts",
"description": "Once the viewer has contacts, seeds stop contributing.",
"behavior": "Following nobody must not be equivalent to following the seed set.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101"
],
"seeds": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303"
]
},
"reports": {
"event:1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b": [
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0202020202020202020202020202020202020202020202020202020202020202",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0303030303030303030303030303030303030303030303030303030303030303",
"category": "nudity",
"createdAt": 1749913600
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"evidence": {
"trustedReportTotal": 1
}
}
}
@@ -0,0 +1,103 @@
{
"name": "spam-report-hide-threshold",
"description": "Five trusted spam reports escalate to a hide.",
"behavior": "Spam carries its own higher hide threshold; other categories do not hide on reports alone.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"reports": {
"event:1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b": [
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "spam",
"createdAt": 1749913600
},
{
"reporter": "0202020202020202020202020202020202020202020202020202020202020202",
"category": "spam",
"createdAt": 1749913600
},
{
"reporter": "0303030303030303030303030303030303030303030303030303030303030303",
"category": "spam",
"createdAt": 1749913600
},
{
"reporter": "0404040404040404040404040404040404040404040404040404040404040404",
"category": "spam",
"createdAt": 1749913600
},
{
"reporter": "0505050505050505050505050505050505050505050505050505050505050505",
"category": "spam",
"createdAt": 1749913600
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "hide",
"overridable": true
},
"interaction": {
"effect": "require-explicit-action"
},
"reasons": [
"trusted-report",
"trusted-report-threshold"
]
}
}
@@ -0,0 +1,156 @@
{
"name": "trusted-mute-at-threshold",
"description": "Twenty trusted mutes escalate to a reversible hide.",
"behavior": "The hide threshold is 20 unique trusted muters.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"trustedMutes": {
"user:d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4": [
{
"muter": "0101010101010101010101010101010101010101010101010101010101010101",
"updatedAt": 1749913600
},
{
"muter": "0202020202020202020202020202020202020202020202020202020202020202",
"updatedAt": 1749913600
},
{
"muter": "0303030303030303030303030303030303030303030303030303030303030303",
"updatedAt": 1749913600
},
{
"muter": "0404040404040404040404040404040404040404040404040404040404040404",
"updatedAt": 1749913600
},
{
"muter": "0505050505050505050505050505050505050505050505050505050505050505",
"updatedAt": 1749913600
},
{
"muter": "0606060606060606060606060606060606060606060606060606060606060606",
"updatedAt": 1749913600
},
{
"muter": "0707070707070707070707070707070707070707070707070707070707070707",
"updatedAt": 1749913600
},
{
"muter": "0808080808080808080808080808080808080808080808080808080808080808",
"updatedAt": 1749913600
},
{
"muter": "0909090909090909090909090909090909090909090909090909090909090909",
"updatedAt": 1749913600
},
{
"muter": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"updatedAt": 1749913600
},
{
"muter": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"updatedAt": 1749913600
},
{
"muter": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"updatedAt": 1749913600
},
{
"muter": "0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"updatedAt": 1749913600
},
{
"muter": "0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"updatedAt": 1749913600
},
{
"muter": "0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"updatedAt": 1749913600
},
{
"muter": "1010101010101010101010101010101010101010101010101010101010101010",
"updatedAt": 1749913600
},
{
"muter": "1111111111111111111111111111111111111111111111111111111111111111",
"updatedAt": 1749913600
},
{
"muter": "1212121212121212121212121212121212121212121212121212121212121212",
"updatedAt": 1749913600
},
{
"muter": "1313131313131313131313131313131313131313131313131313131313131313",
"updatedAt": 1749913600
},
{
"muter": "1414141414141414141414141414141414141414141414141414141414141414",
"updatedAt": 1749913600
}
]
},
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "downrank",
"weight": 1
},
"visibility": {
"effect": "hide",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"evidence": {
"trustedMuteTotal": 20
}
}
}
@@ -0,0 +1,156 @@
{
"name": "trusted-mute-below-threshold",
"description": "Nineteen trusted mutes downrank only.",
"behavior": "Below the hide threshold a trusted mute is a ranking signal: no blur, no playback block, no denial.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"trustedMutes": {
"user:d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4": [
{
"muter": "0101010101010101010101010101010101010101010101010101010101010101",
"updatedAt": 1749913600
},
{
"muter": "0202020202020202020202020202020202020202020202020202020202020202",
"updatedAt": 1749913600
},
{
"muter": "0303030303030303030303030303030303030303030303030303030303030303",
"updatedAt": 1749913600
},
{
"muter": "0404040404040404040404040404040404040404040404040404040404040404",
"updatedAt": 1749913600
},
{
"muter": "0505050505050505050505050505050505050505050505050505050505050505",
"updatedAt": 1749913600
},
{
"muter": "0606060606060606060606060606060606060606060606060606060606060606",
"updatedAt": 1749913600
},
{
"muter": "0707070707070707070707070707070707070707070707070707070707070707",
"updatedAt": 1749913600
},
{
"muter": "0808080808080808080808080808080808080808080808080808080808080808",
"updatedAt": 1749913600
},
{
"muter": "0909090909090909090909090909090909090909090909090909090909090909",
"updatedAt": 1749913600
},
{
"muter": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"updatedAt": 1749913600
},
{
"muter": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"updatedAt": 1749913600
},
{
"muter": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"updatedAt": 1749913600
},
{
"muter": "0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"updatedAt": 1749913600
},
{
"muter": "0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"updatedAt": 1749913600
},
{
"muter": "0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"updatedAt": 1749913600
},
{
"muter": "1010101010101010101010101010101010101010101010101010101010101010",
"updatedAt": 1749913600
},
{
"muter": "1111111111111111111111111111111111111111111111111111111111111111",
"updatedAt": 1749913600
},
{
"muter": "1212121212121212121212121212121212121212121212121212121212121212",
"updatedAt": 1749913600
},
{
"muter": "1313131313131313131313131313131313131313131313131313131313131313",
"updatedAt": 1749913600
}
]
},
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "downrank",
"weight": 1
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"reasons": [
"trusted-mute",
"trusted-mute-threshold"
],
"evidence": {
"trustedMuteTotal": 19
}
}
}
@@ -0,0 +1,87 @@
{
"name": "trusted-report-below-threshold",
"description": "Two trusted reports are not enough to restrict.",
"behavior": "Counts below the blur threshold leave the target fully visible.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"reports": {
"event:1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b": [
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0202020202020202020202020202020202020202020202020202020202020202",
"category": "nudity",
"createdAt": 1749913600
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"reasons": [
"trusted-report"
]
}
}
@@ -0,0 +1,93 @@
{
"name": "trusted-report-threshold-reached",
"description": "Three trusted reports restrict visibility and withhold autoplay.",
"behavior": "Blur threshold and autoplay-block threshold are both 3 trusted reports.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"reports": {
"event:1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b": [
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0202020202020202020202020202020202020202020202020202020202020202",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "0303030303030303030303030303030303030303030303030303030303030303",
"category": "nudity",
"createdAt": 1749913600
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "restrict",
"overridable": true
},
"interaction": {
"effect": "require-explicit-action"
},
"reasons": [
"trusted-report",
"trusted-report-threshold"
]
}
}
@@ -0,0 +1,64 @@
{
"name": "untrusted-reporter-ignored",
"description": "Reports from outside the trust graph do not count.",
"behavior": "Only reports from the viewer's trusted graph are counted.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101"
]
},
"reports": {
"event:1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b": [
{
"reporter": "0101010101010101010101010101010101010101010101010101010101010101",
"category": "nudity",
"createdAt": 1749913600
},
{
"reporter": "f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6f6",
"category": "nudity",
"createdAt": 1749913600
}
]
},
"target": {
"type": "event",
"id": "1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b1b",
"author": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"evidence": {
"trustedReportTotal": 1
}
}
}
@@ -0,0 +1,78 @@
{
"name": "viewer-override-cannot-escalate",
"description": "An override asking for a harsher effect is ignored.",
"behavior": "Overrides only soften.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"viewer": {
"overrides": [
{
"key": "user:d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4",
"visibility": "deny"
}
]
},
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "normal",
"weight": 0
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "allow"
},
"reasons": []
}
}
@@ -0,0 +1,166 @@
{
"name": "viewer-override-softens-hide",
"description": "A viewer override reveals hidden content behind an explicit action.",
"behavior": "Overrides may soften a decision and force explicit interaction; they never escalate.",
"profile": "feed",
"now": 1750000000,
"authority": {
"root": "a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1",
"actors": {
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1": [
"super_admin"
],
"b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2b2": [
"moderator"
],
"c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3c3": [
"curator"
]
},
"protectedActors": [
"a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1a1"
]
},
"trust": {
"contacts": [
"0101010101010101010101010101010101010101010101010101010101010101",
"0202020202020202020202020202020202020202020202020202020202020202",
"0303030303030303030303030303030303030303030303030303030303030303",
"0404040404040404040404040404040404040404040404040404040404040404",
"0505050505050505050505050505050505050505050505050505050505050505",
"0606060606060606060606060606060606060606060606060606060606060606",
"0707070707070707070707070707070707070707070707070707070707070707",
"0808080808080808080808080808080808080808080808080808080808080808",
"0909090909090909090909090909090909090909090909090909090909090909",
"0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"1010101010101010101010101010101010101010101010101010101010101010",
"1111111111111111111111111111111111111111111111111111111111111111",
"1212121212121212121212121212121212121212121212121212121212121212",
"1313131313131313131313131313131313131313131313131313131313131313",
"1414141414141414141414141414141414141414141414141414141414141414",
"1515151515151515151515151515151515151515151515151515151515151515",
"1616161616161616161616161616161616161616161616161616161616161616",
"1717171717171717171717171717171717171717171717171717171717171717",
"1818181818181818181818181818181818181818181818181818181818181818"
]
},
"viewer": {
"overrides": [
{
"key": "user:d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4",
"visibility": "allow"
}
]
},
"trustedMutes": {
"user:d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4": [
{
"muter": "0101010101010101010101010101010101010101010101010101010101010101",
"updatedAt": 1749913600
},
{
"muter": "0202020202020202020202020202020202020202020202020202020202020202",
"updatedAt": 1749913600
},
{
"muter": "0303030303030303030303030303030303030303030303030303030303030303",
"updatedAt": 1749913600
},
{
"muter": "0404040404040404040404040404040404040404040404040404040404040404",
"updatedAt": 1749913600
},
{
"muter": "0505050505050505050505050505050505050505050505050505050505050505",
"updatedAt": 1749913600
},
{
"muter": "0606060606060606060606060606060606060606060606060606060606060606",
"updatedAt": 1749913600
},
{
"muter": "0707070707070707070707070707070707070707070707070707070707070707",
"updatedAt": 1749913600
},
{
"muter": "0808080808080808080808080808080808080808080808080808080808080808",
"updatedAt": 1749913600
},
{
"muter": "0909090909090909090909090909090909090909090909090909090909090909",
"updatedAt": 1749913600
},
{
"muter": "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a",
"updatedAt": 1749913600
},
{
"muter": "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b",
"updatedAt": 1749913600
},
{
"muter": "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c",
"updatedAt": 1749913600
},
{
"muter": "0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d0d",
"updatedAt": 1749913600
},
{
"muter": "0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e",
"updatedAt": 1749913600
},
{
"muter": "0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f",
"updatedAt": 1749913600
},
{
"muter": "1010101010101010101010101010101010101010101010101010101010101010",
"updatedAt": 1749913600
},
{
"muter": "1111111111111111111111111111111111111111111111111111111111111111",
"updatedAt": 1749913600
},
{
"muter": "1212121212121212121212121212121212121212121212121212121212121212",
"updatedAt": 1749913600
},
{
"muter": "1313131313131313131313131313131313131313131313131313131313131313",
"updatedAt": 1749913600
},
{
"muter": "1414141414141414141414141414141414141414141414141414141414141414",
"updatedAt": 1749913600
}
]
},
"target": {
"type": "user",
"pubkey": "d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4d4"
},
"expect": {
"ranking": {
"effect": "downrank",
"weight": 1
},
"visibility": {
"effect": "allow",
"overridable": true
},
"interaction": {
"effect": "require-explicit-action"
},
"reasons": [
"trusted-mute",
"trusted-mute-threshold",
"viewer-override"
]
}
}
+26 -11
View File
@@ -1,21 +1,36 @@
[
"personal-block-precedence",
"user-deny",
"event-deny",
"address-deny",
"allowlist-miss",
"protected-target",
"trusted-report-threshold-reached",
"trusted-report-below-threshold",
"duplicate-reporter-dedupe",
"blocked-reporter-ignored",
"denied-reporter-ignored",
"untrusted-reporter-ignored",
"spam-report-hide-threshold",
"non-spam-reports-never-hide",
"trusted-mute-below-threshold",
"trusted-mute-at-threshold",
"expired-trusted-mute",
"viewer-override",
"duplicate-muter-dedupe",
"personal-block-precedence",
"admin-user-deny",
"admin-event-deny",
"admin-event-deny-applies",
"author-deny-reaches-events",
"admin-address-deny",
"community-blacklist-merge",
"curator-cannot-deny-events",
"revoked-actor-contribution-dropped",
"protected-target",
"moderator-trust-seed",
"seeds-yield-to-real-contacts",
"viewer-override-softens-hide",
"viewer-override-cannot-escalate",
"author-override",
"home-hide-bypass",
"recent-hide-bypass",
"playback-enforces-hide",
"playback-exposes-evidence",
"evidence-redacted-by-default",
"cache-fallback-effective",
"report-category-thresholds",
"mute-aggregation-rules",
"hide-vs-restrict-vs-downrank",
"moderation-decorator-fields"
"clean-target-allowed"
]
+5 -1
View File
@@ -1894,7 +1894,11 @@
}
},
"packages/testing": {
"name": "@nostr-governance/testing"
"name": "@nostr-governance/testing",
"version": "0.1.0",
"dependencies": {
"@nostr-governance/core": "*"
}
}
}
}
+4
View File
@@ -9,6 +9,10 @@
".": {
"types": "./dist/types/src/index.d.ts",
"import": "./src/index.js"
},
"./profile": {
"types": "./dist/types/src/profile.d.ts",
"import": "./src/profile.js"
}
},
"scripts": {
+127
View File
@@ -0,0 +1,127 @@
// Reference application profile for the extraction's characterization corpus.
//
// These values are one application's configuration, not governance defaults.
// They live outside @nostr-governance/core on purpose: the core engine must
// carry no application thresholds, and this file is what proves the generic
// engine can reproduce a real deployment's behavior.
//
// Values characterized from the pinned reference commit (see
// docs/reference-map.md). Changing a number here should break a conformance
// fixture — that is the point.
import { createPolicyDefinition } from "@nostr-governance/core";
/** Trusted reports needed before a thumbnail is blurred. */
export const BLUR_THRESHOLD = 3;
/** Trusted reports needed before automatic playback is withheld. */
export const AUTOPLAY_BLOCK_THRESHOLD = 3;
/** Unique trusted muters needed before a target is hidden. */
export const TRUSTED_MUTE_HIDE_THRESHOLD = 20;
/** Trusted spam reports needed before a target is hidden. */
export const TRUSTED_SPAM_HIDE_THRESHOLD = 5;
/** Trusted mutes older than this are ignored. */
export const TRUSTED_MUTE_WINDOW_DAYS = 60;
export const TRUSTED_MUTE_WINDOW_SECONDS = TRUSTED_MUTE_WINDOW_DAYS * 24 * 60 * 60;
/**
* Report thresholds shared by every surface.
*
* `restrict` at the blur threshold is the blur; `requireExplicitAction` at the
* autoplay threshold is the withheld autoplay. Spam escalates to a hide at its
* own higher threshold, while other categories never hide on reports alone.
*
* @type {Record<string, import('@nostr-governance/core').CategoryThresholds>}
*/
const REPORT_THRESHOLDS = {
default: {
restrict: BLUR_THRESHOLD,
requireExplicitAction: AUTOPLAY_BLOCK_THRESHOLD,
},
spam: {
restrict: BLUR_THRESHOLD,
requireExplicitAction: AUTOPLAY_BLOCK_THRESHOLD,
hide: TRUSTED_SPAM_HIDE_THRESHOLD,
},
};
/**
* Trusted mutes downrank from the first muter and only hide at the threshold.
*
* Below the hide threshold a trusted mute is a ranking signal only: it must not
* blur, must not block playback, and must not read as an administrative denial.
*
* @type {Record<string, import('@nostr-governance/core').CategoryThresholds>}
*/
const MUTE_THRESHOLDS = {
default: {
downrank: 1,
hide: TRUSTED_MUTE_HIDE_THRESHOLD,
},
};
/**
* The reference application's policy definition.
*
* Surfaces differ only in how they treat a hide and whether a viewer may
* override: discovery feeds decline to hard-hide so that a single stale list
* cannot empty a feed, while playback enforces the full decision.
*/
export const REFERENCE_POLICY = createPolicyDefinition({
id: "reference-video-app",
name: "Reference video application",
description: "Characterized from the pinned reference commit. Illustrative, not a default.",
version: "1.0.0",
defaultProfile: "feed",
profiles: {
feed: {
name: "feed",
administrativeDeny: { visibility: "hide", interaction: "deny" },
viewerBlock: { visibility: "hide", interaction: "deny" },
allowViewerOverride: true,
muteWindowSeconds: TRUSTED_MUTE_WINDOW_SECONDS,
reports: REPORT_THRESHOLDS,
mutes: MUTE_THRESHOLDS,
},
// Home and Recent decline to hide: a target that would be hidden is
// downranked and restricted instead, preserving the surface exception.
home: {
name: "home",
administrativeDeny: { visibility: "hide", interaction: "deny" },
viewerBlock: { visibility: "hide", interaction: "deny" },
allowViewerOverride: true,
bypassHide: true,
bypassHideCeiling: "restrict",
muteWindowSeconds: TRUSTED_MUTE_WINDOW_SECONDS,
reports: REPORT_THRESHOLDS,
mutes: MUTE_THRESHOLDS,
},
recent: {
name: "recent",
administrativeDeny: { visibility: "hide", interaction: "deny" },
viewerBlock: { visibility: "hide", interaction: "deny" },
allowViewerOverride: true,
bypassHide: true,
bypassHideCeiling: "restrict",
muteWindowSeconds: TRUSTED_MUTE_WINDOW_SECONDS,
reports: REPORT_THRESHOLDS,
mutes: MUTE_THRESHOLDS,
},
playback: {
name: "playback",
administrativeDeny: { visibility: "deny", interaction: "deny" },
viewerBlock: { visibility: "hide", interaction: "deny" },
allowViewerOverride: true,
exposeEvidence: true,
muteWindowSeconds: TRUSTED_MUTE_WINDOW_SECONDS,
reports: REPORT_THRESHOLDS,
mutes: MUTE_THRESHOLDS,
},
},
});
+255
View File
@@ -0,0 +1,255 @@
// Administrative state reduction for Nostr Governance
//
// Contributions arrive as per-actor lists signed under each actor's own key.
// Reduction resolves each contributor's capabilities against the current
// authority state and keeps only the entries that actor was allowed to publish.
// Because capabilities are resolved here rather than at ingest time, revoking a
// role immediately drops that actor's entries from effective state.
import { getTargetKey } from "./identifiers.js";
import { isValidTarget } from "./targets.js";
import { hasCapability, isProtectedActor } from "./authority.js";
/**
* @typedef {import('./identifiers.js').GovernanceTarget} GovernanceTarget
* @typedef {import('./authority.js').AuthorityState} AuthorityState
* @typedef {import('./authority.js').GovernanceCapability} GovernanceCapability
*/
/**
* @typedef {Object} Contribution
* @property {string} actor - Pubkey of the contributing actor
* @property {"user-allow"|"user-deny"|"event-deny"|"address-deny"|"trust-seed"} kind
* @property {GovernanceTarget[]} targets
* @property {number} [createdAt] - Unix seconds; used for replaceable selection
* @property {string} [source] - Opaque origin marker (e.g. a community list id)
*/
/**
* @typedef {Object} AdminState
* @property {Set<string>} userAllow - Target keys explicitly allowed
* @property {Set<string>} userDeny - Target keys denied
* @property {Set<string>} eventDeny
* @property {Set<string>} addressDeny
* @property {Set<string>} trustSeeds - Pubkeys seeding the trust graph
* @property {Map<string, string[]>} contributors - Target key to contributing actor pubkeys
* @property {Map<string, string[]>} communitySources - Target key to community list ids that denied it
*/
/** @type {Record<Contribution["kind"], GovernanceCapability>} */
const KIND_CAPABILITY = {
"user-allow": "contribute-user-allow",
"user-deny": "contribute-user-deny",
"event-deny": "contribute-event-deny",
"address-deny": "contribute-address-deny",
"trust-seed": "contribute-trust-seed",
};
/** @type {Record<Contribution["kind"], GovernanceTarget["type"]>} */
const KIND_TARGET_TYPE = {
"user-allow": "user",
"user-deny": "user",
"event-deny": "event",
"address-deny": "address",
"trust-seed": "user",
};
/**
* Create an empty administrative state.
* @returns {AdminState}
*/
export function createEmptyAdminState() {
return {
userAllow: new Set(),
userDeny: new Set(),
eventDeny: new Set(),
addressDeny: new Set(),
trustSeeds: new Set(),
contributors: new Map(),
communitySources: new Map(),
};
}
/**
* Reduce contributions into effective administrative state.
*
* Denial entries are a union across authorized contributors, matching the merge
* policy. Protected actors are removed from every denial set as the last step,
* so no contributor list can deny the root administrator regardless of how many
* curators list them.
*
* @param {Contribution[]} contributions
* @param {AuthorityState} authority
* @returns {AdminState}
*/
export function reduceAdminState(contributions, authority) {
const state = createEmptyAdminState();
if (!Array.isArray(contributions)) {
return state;
}
for (const contribution of contributions) {
if (!contribution || typeof contribution !== "object") {
continue;
}
const capability = KIND_CAPABILITY[contribution.kind];
if (!capability) {
continue;
}
if (!hasCapability(contribution.actor, capability, authority)) {
continue;
}
const expectedType = KIND_TARGET_TYPE[contribution.kind];
const targets = Array.isArray(contribution.targets) ? contribution.targets : [];
for (const target of targets) {
// A malformed identifier must never reach a denial set: getTargetKey
// happily formats anything, so validation has to happen here.
if (!target || target.type !== expectedType || !isValidTarget(target)) {
continue;
}
let key;
try {
key = getTargetKey(target);
} catch {
continue;
}
switch (contribution.kind) {
case "user-allow":
state.userAllow.add(key);
break;
case "user-deny":
state.userDeny.add(key);
break;
case "event-deny":
state.eventDeny.add(key);
break;
case "address-deny":
state.addressDeny.add(key);
break;
case "trust-seed":
state.trustSeeds.add(/** @type {{ pubkey: string }} */ (target).pubkey);
break;
}
if (contribution.kind !== "trust-seed") {
const existing = state.contributors.get(key) ?? [];
if (!existing.includes(contribution.actor)) {
existing.push(contribution.actor);
}
state.contributors.set(key, existing);
// Only contributions carrying a source marker are community-curated.
// A moderator acting directly is an administrative denial, not a
// federated one, and the two must stay distinguishable to consumers.
if (typeof contribution.source === "string" && contribution.source.trim()) {
const sources = state.communitySources.get(key) ?? [];
if (!sources.includes(contribution.source)) {
sources.push(contribution.source);
}
state.communitySources.set(key, sources);
}
}
}
}
protectActors(state, authority);
return state;
}
/**
* Remove protected actors from every denial set.
* @param {AdminState} state
* @param {AuthorityState} authority
* @returns {AdminState} The same state, mutated in place
*/
export function protectActors(state, authority) {
for (const pubkey of authority.protectedActors) {
const key = `user:${pubkey}`;
state.userDeny.delete(key);
state.contributors.delete(key);
state.communitySources.delete(key);
}
return state;
}
/**
* Merge a community-curated denial list into administrative state.
*
* Community curators contribute denials without gaining moderator authority:
* the curator must hold `contribute-user-deny`, the merge is a union, and
* protected actors are stripped afterwards.
*
* @param {AdminState} state
* @param {Contribution} contribution
* @param {AuthorityState} authority
* @returns {AdminState} The same state, mutated in place
*/
export function mergeCommunitySource(state, contribution, authority) {
const merged = reduceAdminState([contribution], authority);
for (const key of merged.userDeny) {
state.userDeny.add(key);
const contributors = state.contributors.get(key) ?? [];
for (const actor of merged.contributors.get(key) ?? []) {
if (!contributors.includes(actor)) {
contributors.push(actor);
}
}
state.contributors.set(key, contributors);
const sources = state.communitySources.get(key) ?? [];
for (const source of merged.communitySources.get(key) ?? []) {
if (!sources.includes(source)) {
sources.push(source);
}
}
if (sources.length) {
state.communitySources.set(key, sources);
}
}
return protectActors(state, authority);
}
/**
* Check whether a target is denied by administrative state.
* @param {GovernanceTarget} target
* @param {AdminState} state
* @returns {boolean}
*/
export function isDenied(target, state) {
const key = getTargetKey(target);
switch (target.type) {
case "user":
return state.userDeny.has(key);
case "event":
return state.eventDeny.has(key);
case "address":
return state.addressDeny.has(key);
default:
return false;
}
}
/**
* Serialize administrative state into a plain, ordered object.
*
* Sets are sorted so that fingerprints and fixtures are stable across runs.
*
* @param {AdminState} state
* @returns {{ userAllow: string[], userDeny: string[], eventDeny: string[], addressDeny: string[], trustSeeds: string[] }}
*/
export function serializeAdminState(state) {
return {
userAllow: Array.from(state.userAllow).sort(),
userDeny: Array.from(state.userDeny).sort(),
eventDeny: Array.from(state.eventDeny).sort(),
addressDeny: Array.from(state.addressDeny).sort(),
trustSeeds: Array.from(state.trustSeeds).sort(),
};
}
+208 -228
View File
@@ -1,177 +1,193 @@
// Authority handling for Nostr Governance
// Authority and delegation for Nostr Governance
//
// This module handles role resolution, capability checking, and authority
// validation for the governance system.
// Root-authorized contributor model: the root administrator publishes the role
// roster, each authorized actor publishes contributions under their own key,
// and the runtime accepts a contribution only when the signing actor holds the
// matching capability. Revoking a role stops accepting that actor's
// contributions immediately, because capabilities are resolved at merge time
// rather than baked into stored state.
import { normalizePubkey } from "./identifiers.js";
/**
* @typedef {Object} RoleDefinition
* @property {string} name - Role name
* @property {string[]} capabilities - Array of capability names this role has
* @typedef {"manage-roles"
* | "manage-policy"
* | "manage-community-sources"
* | "contribute-user-allow"
* | "contribute-user-deny"
* | "contribute-event-deny"
* | "contribute-address-deny"
* | "contribute-trust-seed"
* | "review-evidence"} GovernanceCapability
*/
/** @type {readonly GovernanceCapability[]} */
export const GOVERNANCE_CAPABILITIES = Object.freeze([
"manage-roles",
"manage-policy",
"manage-community-sources",
"contribute-user-allow",
"contribute-user-deny",
"contribute-event-deny",
"contribute-address-deny",
"contribute-trust-seed",
"review-evidence",
]);
const CAPABILITY_SET = new Set(GOVERNANCE_CAPABILITIES);
/**
* @typedef {Object} Actor
* @property {string} pubkey - 64-character hex pubkey of the actor
* @property {string[]} roles - Array of role names assigned to this actor
* @param {GovernanceCapability[]} capabilities
* @returns {readonly GovernanceCapability[]}
*/
const bundle = (capabilities) => Object.freeze(capabilities);
/**
* Default role bundles. Roles are convenience only; capabilities are
* authoritative, and applications may define additional roles.
* @type {Record<string, readonly GovernanceCapability[]>}
*/
export const DEFAULT_ROLE_CAPABILITIES = Object.freeze({
super_admin: bundle([
"manage-roles",
"manage-policy",
"manage-community-sources",
"contribute-user-allow",
"contribute-user-deny",
"contribute-event-deny",
"contribute-address-deny",
"contribute-trust-seed",
"review-evidence",
]),
moderator: bundle([
"contribute-user-deny",
"contribute-event-deny",
"contribute-address-deny",
"contribute-trust-seed",
"review-evidence",
]),
curator: bundle(["contribute-user-deny"]),
reviewer: bundle(["review-evidence"]),
});
/**
* @typedef {Object} AuthorityState
* @property {Record<string, RoleDefinition>} roles - Map of role names to definitions
* @property {Record<string, Actor>} actors - Map of pubkeys to actors
* @property {Record<string, readonly GovernanceCapability[]>} roles - Role name to capabilities
* @property {Record<string, string[]>} actors - Actor pubkey to role names
* @property {string[]} protectedActors - Pubkeys that contributor lists cannot deny
* @property {string} [root] - Root administrator pubkey
*/
/**
* Built-in role definitions
* @type {Record<string, RoleDefinition>}
* Check whether a value is a known capability.
* @param {unknown} value
* @returns {value is GovernanceCapability}
*/
export const BUILTIN_ROLES = {
"root": {
name: "root",
capabilities: [
"manage-roles",
"manage-capabilities",
"manage-actors",
"manage-policies",
"manage-admin-lists",
"manage-trust-graph",
"manage-overrides",
"view-reports",
"view-trust-graph",
"view-admin-lists",
"view-policies"
]
},
"administrator": {
name: "administrator",
capabilities: [
"manage-admin-lists",
"manage-trust-graph",
"view-reports",
"view-trust-graph",
"view-admin-lists"
]
},
"moderator": {
name: "moderator",
capabilities: [
"submit-reports",
"submit-trust-endorsements",
"view-reports"
]
},
"trusted-user": {
name: "trusted-user",
capabilities: [
"submit-reports",
"view-reports"
]
},
"user": {
name: "user",
capabilities: [
"submit-reports"
]
export function isGovernanceCapability(value) {
return typeof value === "string" && CAPABILITY_SET.has(/** @type {GovernanceCapability} */ (value));
}
/**
* Create an authority state.
*
* The root administrator is always protected and always holds every
* capability, so a misconfigured role roster cannot lock governance out of its
* own instance.
*
* @param {Object} [options]
* @param {Record<string, readonly GovernanceCapability[]>} [options.roles]
* @param {Record<string, string[]>} [options.actors]
* @param {string[]} [options.protectedActors]
* @param {string} [options.root]
* @returns {AuthorityState}
*/
export function createAuthorityState(options = {}) {
/** @type {Record<string, readonly GovernanceCapability[]>} */
const roles = {};
for (const [name, capabilities] of Object.entries(options.roles ?? DEFAULT_ROLE_CAPABILITIES)) {
const trimmed = name.trim();
if (!trimmed) {
continue;
}
};
roles[trimmed] = Object.freeze(
(Array.isArray(capabilities) ? capabilities : []).filter(isGovernanceCapability),
);
}
/** @type {Record<string, string[]>} */
const actors = {};
for (const [pubkey, roleNames] of Object.entries(options.actors ?? {})) {
const normalized = normalizePubkey(pubkey);
if (!normalized) {
continue;
}
const names = (Array.isArray(roleNames) ? roleNames : [])
.filter((name) => typeof name === "string" && name.trim())
.map((name) => name.trim());
if (names.length) {
actors[normalized] = Array.from(new Set(names));
}
}
const root = options.root ? normalizePubkey(options.root) : "";
const protectedActors = new Set(
(options.protectedActors ?? [])
.map((pubkey) => normalizePubkey(pubkey))
.filter(Boolean),
);
if (root) {
protectedActors.add(root);
}
/** @type {AuthorityState} */
const state = {
roles,
actors,
protectedActors: Array.from(protectedActors).sort(),
};
if (root) {
state.root = root;
}
return state;
}
/**
* Get capabilities for a role
* @param {string} roleName
* @param {AuthorityState} [authorityState]
* Get the role names assigned to an actor.
* @param {string} pubkey
* @param {AuthorityState} authority
* @returns {string[]}
*/
export function getRoleCapabilities(roleName, authorityState) {
// Check custom roles first
if (authorityState && authorityState.roles[roleName]) {
return authorityState.roles[roleName].capabilities;
}
// Check built-in roles
if (BUILTIN_ROLES[roleName]) {
return BUILTIN_ROLES[roleName].capabilities;
}
// Unknown role has no capabilities
export function getActorRoles(pubkey, authority) {
const normalized = normalizePubkey(pubkey);
if (!normalized) {
return [];
}
return [...(authority.actors[normalized] ?? [])];
}
/**
* Check if an actor has a specific role
* @param {string} pubkey - 64-character hex pubkey
* @param {string} roleName
* @param {AuthorityState} [authorityState]
* @returns {boolean}
* Get every capability an actor holds, resolved through their roles.
* @param {string} pubkey
* @param {AuthorityState} authority
* @returns {GovernanceCapability[]}
*/
export function hasRole(pubkey, roleName, authorityState) {
// Normalize pubkey
const normalizedPubkey = pubkey.toLowerCase();
// Check custom actors first
if (authorityState && authorityState.actors[normalizedPubkey]) {
return authorityState.actors[normalizedPubkey].roles.includes(roleName);
}
// No actor found means no roles
return false;
}
/**
* Get all roles for an actor
* @param {string} pubkey - 64-character hex pubkey
* @param {AuthorityState} [authorityState]
* @returns {string[]}
*/
export function getActorRoles(pubkey, authorityState) {
// Normalize pubkey
const normalizedPubkey = pubkey.toLowerCase();
// Check custom actors first
if (authorityState && authorityState.actors[normalizedPubkey]) {
return authorityState.actors[normalizedPubkey].roles;
}
// No actor found means no roles
export function getActorCapabilities(pubkey, authority) {
const normalized = normalizePubkey(pubkey);
if (!normalized) {
return [];
}
/**
* Check if an actor has a specific capability
* @param {string} pubkey - 64-character hex pubkey
* @param {string} capability
* @param {AuthorityState} [authorityState]
* @returns {boolean}
*/
export function hasCapability(pubkey, capability, authorityState) {
// Get all roles for the actor
const roles = getActorRoles(pubkey, authorityState);
// Check if any role has the capability
for (const role of roles) {
const capabilities = getRoleCapabilities(role, authorityState);
if (capabilities.includes(capability)) {
return true;
}
}
return false;
}
if (authority.root && normalized === authority.root) {
return [...GOVERNANCE_CAPABILITIES];
}
/**
* Get all capabilities for an actor
* @param {string} pubkey - 64-character hex pubkey
* @param {AuthorityState} [authorityState]
* @returns {string[]}
*/
export function getActorCapabilities(pubkey, authorityState) {
// Get all roles for the actor
const roles = getActorRoles(pubkey, authorityState);
// Collect all capabilities from all roles
/** @type {Set<GovernanceCapability>} */
const capabilities = new Set();
for (const role of roles) {
const roleCapabilities = getRoleCapabilities(role, authorityState);
for (const capability of roleCapabilities) {
for (const roleName of authority.actors[normalized] ?? []) {
for (const capability of authority.roles[roleName] ?? []) {
capabilities.add(capability);
}
}
@@ -180,99 +196,63 @@ export function getActorCapabilities(pubkey, authorityState) {
}
/**
* Create a new role definition
* @param {string} name - Role name
* @param {string[]} capabilities - Array of capability names
* @returns {RoleDefinition}
* Check whether an actor holds a capability.
* @param {string} pubkey
* @param {GovernanceCapability} capability
* @param {AuthorityState} authority
* @returns {boolean}
*/
export function hasCapability(pubkey, capability, authority) {
if (!isGovernanceCapability(capability)) {
return false;
}
return getActorCapabilities(pubkey, authority).includes(capability);
}
/**
* Check whether an actor holds a role.
* @param {string} pubkey
* @param {string} roleName
* @param {AuthorityState} authority
* @returns {boolean}
*/
export function hasRole(pubkey, roleName, authority) {
return getActorRoles(pubkey, authority).includes(roleName);
}
/**
* Check whether a pubkey is protected from contributor denial.
* @param {string} pubkey
* @param {AuthorityState} authority
* @returns {boolean}
*/
export function isProtectedActor(pubkey, authority) {
const normalized = normalizePubkey(pubkey);
if (!normalized) {
return false;
}
return authority.protectedActors.includes(normalized);
}
/**
* Define a role's capability bundle, rejecting unknown capabilities.
* @param {string} name
* @param {GovernanceCapability[]} capabilities
* @returns {{ name: string, capabilities: GovernanceCapability[] }}
*/
export function createRoleDefinition(name, capabilities) {
// Validate input
if (typeof name !== "string" || !name.trim()) {
throw new Error("Role name must be a non-empty string");
}
if (!Array.isArray(capabilities)) {
throw new Error("Capabilities must be an array");
throw new Error("Role capabilities must be an array");
}
// Validate each capability
for (const capability of capabilities) {
if (typeof capability !== "string" || !capability.trim()) {
throw new Error("All capabilities must be non-empty strings");
if (!isGovernanceCapability(capability)) {
throw new Error(`Unknown governance capability: ${String(capability)}`);
}
}
return {
name: name.trim(),
capabilities: capabilities.map(c => c.trim())
};
}
/**
* Create a new actor
* @param {string} pubkey - 64-character hex pubkey
* @param {string[]} roles - Array of role names
* @returns {Actor}
*/
export function createActor(pubkey, roles) {
// Validate pubkey
if (typeof pubkey !== "string" || !/^[0-9a-f]{64}$/i.test(pubkey)) {
throw new Error("Pubkey must be a 64-character hex string");
}
// Validate roles
if (!Array.isArray(roles)) {
throw new Error("Roles must be an array");
}
// Validate each role
for (const role of roles) {
if (typeof role !== "string" || !role.trim()) {
throw new Error("All roles must be non-empty strings");
}
}
return {
pubkey: pubkey.toLowerCase(),
roles: roles.map(r => r.trim())
};
}
/**
* Create a new authority state
* @param {Record<string, RoleDefinition>} [roles]
* @param {Record<string, Actor>} [actors]
* @returns {AuthorityState}
*/
export function createAuthorityState(roles = {}, actors = {}) {
// Validate roles
for (const [name, role] of Object.entries(roles)) {
if (typeof name !== "string" || !name.trim()) {
throw new Error("Role names must be non-empty strings");
}
if (!role || typeof role !== "object") {
throw new Error("Role definitions must be objects");
}
if (role.name !== name) {
throw new Error(`Role name mismatch: ${name} !== ${role.name}`);
}
}
// Validate actors
for (const [pubkey, actor] of Object.entries(actors)) {
if (typeof pubkey !== "string" || !/^[0-9a-f]{64}$/i.test(pubkey)) {
throw new Error("Actor pubkeys must be 64-character hex strings");
}
if (!actor || typeof actor !== "object") {
throw new Error("Actors must be objects");
}
if (actor.pubkey !== pubkey.toLowerCase()) {
throw new Error(`Actor pubkey mismatch: ${pubkey} !== ${actor.pubkey}`);
}
}
return {
roles,
actors
};
return { name: name.trim(), capabilities: [...new Set(capabilities)] };
}
+516 -356
View File
@@ -1,441 +1,601 @@
// Evaluator for Nostr Governance
// Canonical evaluator for Nostr Governance
//
// This module brings together all the core components to evaluate governance
// decisions for targets based on state and policies.
// This is the single place policy is decided. Applications collect targets and
// apply the returned decision; they must not recompute thresholds themselves.
//
// Evaluation is pure: it performs no I/O, reads an injected clock, and never
// mutates its inputs. The same snapshot and context always produce the same
// decision, which is what makes the conformance corpus meaningful.
import { getTargetKey } from "./identifiers.js";
import { areTargetsEqual, isDescendantOf } from "./targets.js";
import { hasCapability } from "./authority.js";
import { isValidTarget } from "./targets.js";
import { isProtectedActor } from "./authority.js";
import { isDenied } from "./adminState.js";
import { createEmptyEvidence, freezeEvidence, redactEvidence } from "./evidence.js";
import { createReason, dedupeReasons } from "./reasons.js";
import {
createGovernanceDecision,
getPolicyEffect,
composeGovernanceDecisions
NEUTRAL_POLICY,
applyEffects,
applyThresholds,
createNeutralDecision,
effectSeverity,
resolveProfile,
resolveThresholds,
} from "./policy.js";
import { snapshotFingerprint } from "./fingerprint.js";
/**
* @typedef {import('./identifiers.js').GovernanceTarget} GovernanceTarget
* @typedef {import('./authority.js').AuthorityState} AuthorityState
* @typedef {import('./adminState.js').AdminState} AdminState
* @typedef {import('./policy.js').PolicyContext} PolicyContext
* @typedef {import('./policy.js').PolicyProfile} PolicyProfile
* @typedef {import('./policy.js').GovernanceDecision} GovernanceDecision
* @typedef {import('./policy.js').PolicyDefinition} PolicyDefinition
* @typedef {import('./evidence.js').GovernanceEvidence} GovernanceEvidence
* @typedef {import('./reasons.js').GovernanceReason} GovernanceReason
*/
/**
* @typedef {Object} Report
* @property {string} reporter - Pubkey of the reporter
* @property {GovernanceTarget} target - Target being reported
* @typedef {Object} ReportRecord
* @property {string} reporter - Reporter pubkey
* @property {string} category - Report category
* @property {number} timestamp - Unix timestamp of the report
* @property {number} [createdAt] - Unix seconds
*/
/**
* @typedef {Object} TrustedMute
* @property {GovernanceTarget} target - Target being muted
* @property {number} count - Number of trusted mutes
* @property {Record<string, number>} categories - Count of mutes per category
* @typedef {Object} MuteRecord
* @property {string} muter - Muting pubkey
* @property {string} [category] - Optional mute category
* @property {number} [updatedAt] - Unix seconds; compared against the mute window
*/
/**
* @typedef {Object} Override
* @property {GovernanceTarget} target - Target being overridden
* @property {"allow"|"restrict"|"hide"|"deny"} visibility - Visibility override
* @property {string} [reason] - Reason for the override
* @typedef {Object} ViewerState
* @property {Set<string>} [blocks] - Pubkeys the viewer blocks outright
* @property {Set<string>} [mutes] - Pubkeys the viewer mutes
* @property {Map<string, { visibility: string, reason?: string }>} [overrides] - Viewer overrides by target key
*/
/**
* @typedef {Object} GovernanceState
* @property {AuthorityState} authority - Authority state
* @property {Record<string, Report[]>} reports - Reports by target key
* @property {Record<string, TrustedMute>} trustedMutes - Trusted mutes by target key
* @property {Record<string, Override>} overrides - Overrides by target key
* @property {Record<string, PolicyDefinition>} policies - Policy definitions by ID
* @typedef {Object} TrustState
* @property {Set<string>} [contacts] - Pubkeys the viewer trusts directly
* @property {Set<string>} [seeds] - Fallback trust seeds for anonymous viewers
*/
/**
* Default thresholds for governance decisions
* @typedef {Object} GovernanceSnapshot
* @property {AuthorityState} authority
* @property {AdminState} admin
* @property {TrustState} [trust]
* @property {Map<string, ReportRecord[]>} [reports] - Reports by target key
* @property {Map<string, MuteRecord[]>} [trustedMutes] - Mutes by target key
*/
export const DEFAULT_THRESHOLDS = {
// Number of trusted reports needed to hide a target
trustedReportHideThreshold: 5,
// Number of trusted reports needed to restrict a target
trustedReportRestrictThreshold: 3,
// Number of trusted mutes needed to hide a target
trustedMuteHideThreshold: 10,
// Number of trusted mutes needed to restrict a target
trustedMuteRestrictThreshold: 5
};
/**
* Evaluate a single target against the governance state
* Resolve the effective trust set for a viewer.
*
* Trust seeds are a fallback for anonymous or not-yet-hydrated viewers, not an
* addition to a real follow graph: once the viewer has contacts, seeds stop
* contributing so that following nobody is not equivalent to following the
* seed set.
*
* @param {TrustState} [trust]
* @returns {Set<string>}
*/
export function resolveTrustSet(trust) {
const contacts = trust?.contacts;
if (contacts && contacts.size > 0) {
return contacts;
}
return trust?.seeds ?? new Set();
}
/**
* Decide whether a contributor's signal counts.
*
* Blocked and administratively denied accounts are excluded before any
* counting, so a denied account cannot manufacture reports.
*
* @param {string} pubkey
* @param {Set<string>} trustSet
* @param {GovernanceSnapshot} snapshot
* @param {ViewerState} viewer
* @returns {boolean}
*/
function isCountableSignal(pubkey, trustSet, snapshot, viewer) {
if (typeof pubkey !== "string" || !pubkey) {
return false;
}
if (!trustSet.has(pubkey)) {
return false;
}
if (viewer.blocks?.has(pubkey)) {
return false;
}
if (snapshot.admin.userDeny.has(`user:${pubkey}`)) {
return false;
}
return true;
}
/**
* Aggregate trusted reports for a target.
*
* Reports are deduplicated per (reporter, category): one account reporting the
* same target for the same reason twice is one signal, but the same account may
* legitimately report distinct categories.
*
* @param {string} key
* @param {GovernanceSnapshot} snapshot
* @param {ViewerState} viewer
* @param {Set<string>} trustSet
* @returns {{ total: number, byCategory: Record<string, number>, reporters: string[] }}
*/
export function aggregateReports(key, snapshot, viewer, trustSet) {
const records = snapshot.reports?.get(key) ?? [];
/** @type {Map<string, Set<string>>} */
const byCategory = new Map();
/** @type {Set<string>} */
const reporters = new Set();
for (const record of records) {
if (!record || !isCountableSignal(record.reporter, trustSet, snapshot, viewer)) {
continue;
}
const category = typeof record.category === "string" ? record.category.trim().toLowerCase() : "";
if (!category) {
continue;
}
if (!byCategory.has(category)) {
byCategory.set(category, new Set());
}
/** @type {Set<string>} */ (byCategory.get(category)).add(record.reporter);
reporters.add(record.reporter);
}
/** @type {Record<string, number>} */
const counts = {};
let total = 0;
for (const [category, set] of byCategory.entries()) {
counts[category] = set.size;
total += set.size;
}
return { total, byCategory: counts, reporters: Array.from(reporters) };
}
/**
* Aggregate trusted mutes for a target.
*
* Muters are counted uniquely and mutes outside the profile's validity window
* are ignored, so a stale list cannot hold a target down indefinitely.
*
* @param {string} key
* @param {GovernanceSnapshot} snapshot
* @param {ViewerState} viewer
* @param {Set<string>} trustSet
* @param {number} now - Unix seconds
* @param {number} [windowSeconds] - 0 or undefined disables expiry
* @returns {{ total: number, byCategory: Record<string, number>, muters: string[] }}
*/
export function aggregateMutes(key, snapshot, viewer, trustSet, now, windowSeconds) {
const records = snapshot.trustedMutes?.get(key) ?? [];
const cutoff = Number.isFinite(windowSeconds) && /** @type {number} */ (windowSeconds) > 0
? now - /** @type {number} */ (windowSeconds)
: Number.NEGATIVE_INFINITY;
/** @type {Set<string>} */
const muters = new Set();
/** @type {Map<string, Set<string>>} */
const byCategory = new Map();
for (const record of records) {
if (!record || !isCountableSignal(record.muter, trustSet, snapshot, viewer)) {
continue;
}
const updatedAt = Number.isFinite(record.updatedAt) ? /** @type {number} */ (record.updatedAt) : 0;
if (updatedAt < cutoff) {
continue;
}
muters.add(record.muter);
const category = typeof record.category === "string" ? record.category.trim().toLowerCase() : "";
if (category) {
if (!byCategory.has(category)) {
byCategory.set(category, new Set());
}
/** @type {Set<string>} */ (byCategory.get(category)).add(record.muter);
}
}
/** @type {Record<string, number>} */
const counts = {};
for (const [category, set] of byCategory.entries()) {
counts[category] = set.size;
}
return { total: muters.size, byCategory: counts, muters: Array.from(muters) };
}
/**
* Collect the pubkeys whose administrative denial should reach this target.
*
* An event or address inherits its author's denial: denying a seller must take
* down that seller's listings. An exact-event denial does not travel the other
* way — it applies only to the event named.
*
* @param {GovernanceTarget} target
* @param {GovernanceState} state
* @param {PolicyContext} context
* @param {string} [viewerPubkey] - Pubkey of the viewer (for personal overrides)
* @returns {string[]}
*/
function authorChain(target) {
if (target.type === "user") {
return [target.pubkey];
}
if (target.type === "address") {
return [target.pubkey];
}
const author = /** @type {{ author?: string }} */ (target).author;
return typeof author === "string" && author ? [author] : [];
}
/**
* Evaluate one target.
*
* @param {GovernanceTarget} target
* @param {GovernanceSnapshot} snapshot
* @param {PolicyContext} [context]
* @param {ViewerState} [viewerState]
* @returns {GovernanceDecision}
*/
export function evaluateTarget(target, state, context, viewerPubkey) {
const decisions = [];
// 1. Check for personal overrides first
if (viewerPubkey) {
const overrideDecision = checkPersonalOverrides(target, state, viewerPubkey);
if (overrideDecision) {
decisions.push(overrideDecision);
}
export function evaluateTarget(target, snapshot, context = { surface: "default" }, viewerState = {}) {
// 1. Validate and normalize the target.
if (!isValidTarget(target)) {
throw new Error(`Invalid governance target: ${JSON.stringify(target)}`);
}
// 2. Check for explicit overrides
const explicitOverrideDecision = checkExplicitOverrides(target, state, context);
if (explicitOverrideDecision) {
decisions.push(explicitOverrideDecision);
const key = getTargetKey(target);
const policy = context.policy ?? NEUTRAL_POLICY;
const profile = resolveProfile(policy, context);
const now = Number.isFinite(context.now) ? /** @type {number} */ (context.now) : 0;
const viewer = viewerState ?? {};
const includeTransaction =
profile.administrativeDeny?.transaction !== undefined ||
hasTransactionGate(profile);
/** @type {GovernanceDecision} */
const decision = createNeutralDecision({ key, target, includeTransaction });
decision.policyProfile = profile.name;
decision.policyVersion = policy.version;
decision.evaluatedAt = now;
decision.snapshotFingerprint = snapshotFingerprint({
authority: snapshot.authority,
admin: snapshot.admin,
reports: snapshot.reports,
trustedMutes: snapshot.trustedMutes,
policy: { id: policy.id, version: policy.version, profile: profile.name },
});
const evidence = createEmptyEvidence();
// A disabled surface reports why it decided nothing rather than silently
// returning a neutral decision that looks like a real evaluation.
if (profile.disabled) {
decision.reasons = [createReason("policy-disabled")];
decision.evidence = evidence;
return decision;
}
// 3. Check reports
const reportDecision = checkReports(target, state, context);
if (reportDecision) {
decisions.push(reportDecision);
}
const authors = authorChain(target);
// 4. Check trusted mutes
const muteDecision = checkTrustedMutes(target, state, context);
if (muteDecision) {
decisions.push(muteDecision);
}
// 2. Protected-target status. Checked before any denial path so that a
// protected actor cannot be denied even by a malformed admin snapshot.
const isProtected = authors.some((pubkey) => isProtectedActor(pubkey, snapshot.authority));
evidence.protectedTarget = isProtected;
// 5. Check if target is a descendant of a denied ancestor
const ancestorDecision = checkAncestorDeny(target, state, context);
if (ancestorDecision) {
decisions.push(ancestorDecision);
}
// Compose all decisions into a final decision
if (decisions.length === 0) {
// Default allow decision
return createGovernanceDecision(
{ effect: /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("allow") },
{ effect: /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("allow") }
// 3. Viewer personal block. Viewer-local and always enforced: a viewer's own
// block is never softened by an application profile.
const blockedAuthor = authors.find((pubkey) => viewer.blocks?.has(pubkey));
if (blockedAuthor) {
evidence.personalBlock = true;
applyEffects(
decision,
profile.viewerBlock ?? { visibility: "hide", interaction: "deny" },
[createReason("viewer-block")],
);
}
return composeGovernanceDecisions(decisions);
}
/**
* Check for personal overrides for a viewer
* @param {GovernanceTarget} target
* @param {GovernanceState} state
* @param {string} viewerPubkey
* @returns {GovernanceDecision|null}
*/
function checkPersonalOverrides(target, state, viewerPubkey) {
// This would check for personal allowlists, blocklists, etc.
// For now, we'll return null to indicate no personal override
return null;
}
/**
* Check for explicit overrides
* @param {GovernanceTarget} target
* @param {GovernanceState} state
* @param {PolicyContext} context
* @returns {GovernanceDecision|null}
*/
function checkExplicitOverrides(target, state, context) {
const targetKey = getTargetKey(target);
const override = state.overrides[targetKey];
if (!override) {
return null;
if (authors.some((pubkey) => viewer.mutes?.has(pubkey))) {
evidence.personalMute = true;
decision.reasons = dedupeReasons([...decision.reasons, createReason("viewer-mute")]);
}
// Check if the override should be applied based on context
if (context.enforcement && context.enforcement.allowOverrides === false) {
return null;
// 4-6. Administrative denial: the target itself, then its author.
if (!isProtected) {
const targetDenied = isDenied(target, snapshot.admin);
const authorDenied =
target.type !== "user" && authors.some((pubkey) => snapshot.admin.userDeny.has(`user:${pubkey}`));
if (target.type === "user" && targetDenied) {
evidence.userDenied = true;
}
if (target.type === "event" && targetDenied) {
evidence.eventDenied = true;
}
if (target.type === "address" && targetDenied) {
evidence.addressDenied = true;
}
if (authorDenied) {
evidence.userDenied = true;
}
const reasons = [];
if (override.reason) {
reasons.push(override.reason);
if (targetDenied || authorDenied) {
const reasonId =
target.type === "event" && targetDenied
? "admin-event-deny"
: target.type === "address" && targetDenied
? "admin-address-deny"
: "admin-user-deny";
const communitySources = snapshot.admin.communitySources.get(key) ?? [];
/** @type {GovernanceReason[]} */
const reasons = [createReason(reasonId)];
if (communitySources.length) {
reasons.push(createReason("community-user-deny", { source: communitySources[0] }));
}
return createGovernanceDecision(
{
effect: /** @type {import('./policy.js').PolicyEffect["effect"]} */ (override.visibility),
reason: override.reason
},
{
effect: /** @type {import('./policy.js').PolicyEffect["effect"]} */ (override.visibility),
reason: override.reason
},
undefined,
applyEffects(
decision,
profile.administrativeDeny ?? { visibility: "hide", interaction: "deny" },
reasons,
[override]
);
}
/**
* Check reports against a target
* @param {GovernanceTarget} target
* @param {GovernanceState} state
* @param {PolicyContext} context
* @returns {GovernanceDecision|null}
*/
function checkReports(target, state, context) {
const targetKey = getTargetKey(target);
const targetReports = state.reports[targetKey] || [];
if (targetReports.length === 0) {
return null;
}
} else if (isDenied(target, snapshot.admin)) {
// A protected actor appearing in a denial set is a policy no-op, but the
// consumer should be able to see that it was ignored.
decision.reasons = dedupeReasons([...decision.reasons, createReason("protected-target")]);
}
// Count trusted reports by category
/** @type {Record<string, number>} */
const trustedReportCounts = {};
/** @type {any[]} */
const evidence = [];
for (const report of targetReports) {
// Check if the reporter is trusted
if (hasCapability(report.reporter, "submit-reports", state.authority)) {
if (!trustedReportCounts[report.category]) {
trustedReportCounts[report.category] = 0;
}
trustedReportCounts[report.category]++;
evidence.push(report);
}
}
// Determine effect based on trusted report counts
let visibilityEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("allow");
let interactionEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("allow");
/** @type {string[]} */
const reasons = [];
for (const [category, count] of Object.entries(trustedReportCounts)) {
if (count >= DEFAULT_THRESHOLDS.trustedReportHideThreshold) {
visibilityEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("hide");
interactionEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("deny");
reasons.push(`Hidden due to ${count} trusted reports in category ${category}`);
} else if (count >= DEFAULT_THRESHOLDS.trustedReportRestrictThreshold) {
visibilityEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("restrict");
reasons.push(`Restricted due to ${count} trusted reports in category ${category}`);
}
}
if (visibilityEffect === "allow" && interactionEffect === "allow") {
return null;
}
return createGovernanceDecision(
{ effect: visibilityEffect, reason: reasons.join("; ") },
{ effect: interactionEffect, reason: reasons.join("; ") },
undefined,
reasons,
evidence
// 7. Allowlist policy. Access control is separate from trust: being allowed
// to publish grants no trust, and missing the allowlist is not a moral
// judgement, so it is only enforced where the profile asks for it.
if (profile.requireAllowlist && !isProtected) {
const allowed = authors.some((pubkey) => snapshot.admin.userAllow.has(`user:${pubkey}`));
evidence.userAllowed = allowed;
if (!allowed) {
applyEffects(
decision,
profile.allowlistMiss ?? { visibility: "hide", interaction: "deny" },
[createReason("allowlist-miss")],
);
}
/**
* Check trusted mutes against a target
* @param {GovernanceTarget} target
* @param {GovernanceState} state
* @param {PolicyContext} context
* @returns {GovernanceDecision|null}
*/
function checkTrustedMutes(target, state, context) {
const targetKey = getTargetKey(target);
const trustedMute = state.trustedMutes[targetKey];
if (!trustedMute) {
return null;
}
} else {
evidence.userAllowed = authors.some((pubkey) => snapshot.admin.userAllow.has(`user:${pubkey}`));
}
// Determine effect based on trusted mute count
let visibilityEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("allow");
let interactionEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("allow");
/** @type {string[]} */
const reasons = [];
const evidence = [trustedMute];
// 9-11. Trust signals. Aggregated after administrative state so that denied
// accounts are already excluded from counting.
const trustSet = resolveTrustSet(snapshot.trust);
if (trustedMute.count >= DEFAULT_THRESHOLDS.trustedMuteHideThreshold) {
visibilityEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("hide");
interactionEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("deny");
reasons.push(`Hidden due to ${trustedMute.count} trusted mutes`);
} else if (trustedMute.count >= DEFAULT_THRESHOLDS.trustedMuteRestrictThreshold) {
visibilityEffect = /** @type {import('./policy.js').PolicyEffect["effect"]} */ ("restrict");
reasons.push(`Restricted due to ${trustedMute.count} trusted mutes`);
}
const mutes = aggregateMutes(key, snapshot, viewer, trustSet, now, profile.muteWindowSeconds);
evidence.trustedMuteTotal = mutes.total;
evidence.trustedMutesByCategory = mutes.byCategory;
evidence.trustedMuterPubkeys = mutes.muters;
if (visibilityEffect === "allow" && interactionEffect === "allow") {
return null;
}
return createGovernanceDecision(
{ effect: visibilityEffect, reason: reasons.join("; ") },
{ effect: interactionEffect, reason: reasons.join("; ") },
undefined,
reasons,
evidence
if (mutes.total > 0) {
const thresholds = resolveThresholds(profile.mutes, undefined);
const { effects, firedGates } = applyThresholds(mutes.total, thresholds);
/** @type {GovernanceReason[]} */
const reasons = [createReason("trusted-mute", { count: mutes.total })];
if (firedGates.length) {
reasons.push(
createReason("trusted-mute-threshold", {
count: mutes.total,
threshold: lowestFiredThreshold(thresholds, firedGates),
}),
);
}
applyEffects(decision, effects, reasons);
for (const [category, count] of Object.entries(mutes.byCategory)) {
const categoryThresholds = profile.mutes?.[category];
if (!categoryThresholds) {
continue;
}
const categoryResult = applyThresholds(count, categoryThresholds);
if (categoryResult.firedGates.length) {
applyEffects(decision, categoryResult.effects, [
createReason("trusted-mute-threshold", {
category,
count,
threshold: lowestFiredThreshold(categoryThresholds, categoryResult.firedGates),
}),
]);
}
}
}
const reports = aggregateReports(key, snapshot, viewer, trustSet);
evidence.trustedReportTotal = reports.total;
evidence.trustedReportsByCategory = reports.byCategory;
evidence.trustedReporterPubkeys = reports.reporters;
if (reports.total > 0) {
decision.reasons = dedupeReasons([
...decision.reasons,
createReason("trusted-report", { count: reports.total }),
]);
for (const [category, count] of Object.entries(reports.byCategory)) {
const thresholds = resolveThresholds(profile.reports, category);
const { effects, firedGates } = applyThresholds(count, thresholds);
if (!firedGates.length) {
continue;
}
applyEffects(decision, effects, [
createReason("trusted-report-threshold", {
category,
count,
threshold: lowestFiredThreshold(thresholds, firedGates),
}),
]);
}
}
evidence.thresholds = summarizeThresholds(profile);
// 8. Viewer override, applied after evidence so the viewer is overriding a
// decision that actually exists. Overrides may only soften, never
// escalate, and never reach a non-overridable decision.
const override = viewer.overrides?.get(key);
const overridesAllowed =
context.enforcement?.allowOverrides !== false && profile.allowViewerOverride !== false;
if (override && overridesAllowed && decision.visibility.overridable) {
const requested = override.visibility;
if (effectSeverity("visibility", requested) >= 0) {
const isSoftening =
effectSeverity("visibility", requested) < effectSeverity("visibility", decision.visibility.effect);
if (isSoftening) {
decision.visibility.effect = /** @type {import('./policy.js').VisibilityEffect} */ (requested);
decision.interaction.effect = "require-explicit-action";
decision.reasons = dedupeReasons([...decision.reasons, createReason("viewer-override")]);
}
}
}
// 13. Surface policy profile. A feed may decline to hide while still
// downranking, so the hide ceiling is applied last, over the composed
// decision, and records that it fired.
if (profile.bypassHide && effectSeverity("visibility", decision.visibility.effect) >= effectSeverity("visibility", "hide")) {
const ceiling = profile.bypassHideCeiling ?? "restrict";
decision.visibility.effect = /** @type {import('./policy.js').VisibilityEffect} */ (ceiling);
decision.reasons = dedupeReasons([...decision.reasons, createReason("surface-policy-bypass")]);
}
decision.evidence = profile.exposeEvidence ? freezeEvidence(evidence) : redactEvidence(evidence);
decision.reasons = dedupeReasons(decision.reasons);
return decision;
}
/**
* Check if target is a descendant of a denied ancestor
* @param {GovernanceTarget} target
* @param {GovernanceState} state
* @param {PolicyContext} context
* @returns {GovernanceDecision|null}
* Whether any category threshold in a profile can affect the transaction
* dimension, which decides if the decision carries a transaction block at all.
* @param {PolicyProfile} profile
* @returns {boolean}
*/
function checkAncestorDeny(target, state, context) {
// This would check if any ancestor of the target is denied
// For now, we'll return null to indicate no ancestor deny
return null;
function hasTransactionGate(profile) {
for (const table of [profile.reports, profile.mutes]) {
for (const thresholds of Object.values(table ?? {})) {
if (
Number.isFinite(thresholds.transactionDeny) ||
Number.isFinite(thresholds.transactionReview)
) {
return true;
}
}
}
return false;
}
/**
* Evaluate multiple targets against the governance state
* The smallest threshold value among the gates that fired, which is the one
* that explains why the decision changed.
* @param {import('./policy.js').CategoryThresholds} thresholds
* @param {string[]} firedGates
* @returns {number|undefined}
*/
function lowestFiredThreshold(thresholds, firedGates) {
let lowest;
for (const gate of firedGates) {
const value = /** @type {Record<string, number|undefined>} */ (thresholds)[gate];
if (Number.isFinite(value) && (lowest === undefined || /** @type {number} */ (value) < lowest)) {
lowest = /** @type {number} */ (value);
}
}
return lowest;
}
/**
* Summarize a profile's headline thresholds for evidence reporting.
* @param {PolicyProfile} profile
* @returns {import('./evidence.js').EvidenceThresholds}
*/
function summarizeThresholds(profile) {
const defaults = profile.reports?.default ?? {};
/** @type {import('./evidence.js').EvidenceThresholds} */
const summary = {};
for (const gate of /** @type {const} */ (["warn", "restrict", "hide", "deny"])) {
if (Number.isFinite(defaults[gate])) {
summary[gate] = /** @type {number} */ (defaults[gate]);
}
}
return summary;
}
/**
* Evaluate many targets against one snapshot.
*
* The snapshot fingerprint is computed once and reused, which is what keeps
* large feeds from re-hashing identical state per item.
*
* @param {GovernanceTarget[]} targets
* @param {GovernanceState} state
* @param {PolicyContext} context
* @param {string} [viewerPubkey] - Pubkey of the viewer (for personal overrides)
* @returns {Record<string, GovernanceDecision>}
* @param {GovernanceSnapshot} snapshot
* @param {PolicyContext} [context]
* @param {ViewerState} [viewerState]
* @returns {Map<string, GovernanceDecision>}
*/
export function evaluateMany(targets, state, context, viewerPubkey) {
/** @type {Record<string, GovernanceDecision>} */
const results = {};
export function evaluateMany(targets, snapshot, context, viewerState) {
/** @type {Map<string, GovernanceDecision>} */
const results = new Map();
for (const target of targets) {
const targetKey = getTargetKey(target);
results[targetKey] = evaluateTarget(target, state, context, viewerPubkey);
if (!isValidTarget(target)) {
continue;
}
const key = getTargetKey(target);
if (results.has(key)) {
continue;
}
results.set(key, evaluateTarget(target, snapshot, context, viewerState));
}
return results;
}
/**
* Create a governance state
* @param {AuthorityState} [authority]
* @param {Record<string, Report[]>} [reports]
* @param {Record<string, TrustedMute>} [trustedMutes]
* @param {Record<string, Override>} [overrides]
* @param {Record<string, PolicyDefinition>} [policies]
* @returns {GovernanceState}
* Create a governance snapshot with empty defaults.
* @param {Partial<GovernanceSnapshot>} [parts]
* @returns {GovernanceSnapshot}
*/
export function createGovernanceState(authority, reports, trustedMutes, overrides, policies) {
export function createSnapshot(parts = {}) {
return {
authority: authority || { roles: {}, actors: {} },
reports: reports || {},
trustedMutes: trustedMutes || {},
overrides: overrides || {},
policies: policies || {}
authority: parts.authority ?? { roles: {}, actors: {}, protectedActors: [] },
admin: parts.admin ?? {
userAllow: new Set(),
userDeny: new Set(),
eventDeny: new Set(),
addressDeny: new Set(),
trustSeeds: new Set(),
contributors: new Map(),
communitySources: new Map(),
},
trust: parts.trust ?? { contacts: new Set(), seeds: new Set() },
reports: parts.reports ?? new Map(),
trustedMutes: parts.trustedMutes ?? new Map(),
};
}
/**
* Create a report
* @param {string} reporter
* @param {GovernanceTarget} target
* @param {string} category
* @param {number} timestamp
* @returns {Report}
* Create a viewer state with empty defaults.
* @param {Partial<ViewerState>} [parts]
* @returns {ViewerState}
*/
export function createReport(reporter, target, category, timestamp) {
// Validate reporter pubkey
if (typeof reporter !== "string" || !/^[0-9a-f]{64}$/i.test(reporter)) {
throw new Error("Reporter must be a 64-character hex pubkey");
}
// Validate target
if (!target || typeof target !== "object") {
throw new Error("Target must be an object");
}
// Validate category
if (typeof category !== "string" || !category.trim()) {
throw new Error("Category must be a non-empty string");
}
// Validate timestamp
if (typeof timestamp !== "number" || timestamp <= 0) {
throw new Error("Timestamp must be a positive number");
}
export function createViewerState(parts = {}) {
return {
reporter: reporter.toLowerCase(),
target,
category: category.trim(),
timestamp
blocks: parts.blocks ?? new Set(),
mutes: parts.mutes ?? new Set(),
overrides: parts.overrides ?? new Map(),
};
}
/**
* Create a trusted mute
* @param {GovernanceTarget} target
* @param {number} count
* @param {Record<string, number>} categories
* @returns {TrustedMute}
*/
export function createTrustedMute(target, count, categories) {
// Validate target
if (!target || typeof target !== "object") {
throw new Error("Target must be an object");
}
// Validate count
if (typeof count !== "number" || count < 0) {
throw new Error("Count must be a non-negative number");
}
// Validate categories
if (!categories || typeof categories !== "object") {
throw new Error("Categories must be an object");
}
// Validate each category count
for (const [category, categoryCount] of Object.entries(categories)) {
if (typeof category !== "string" || !category.trim()) {
throw new Error("Category names must be non-empty strings");
}
if (typeof categoryCount !== "number" || categoryCount < 0) {
throw new Error("Category counts must be non-negative numbers");
}
}
return {
target,
count,
categories
};
}
/**
* Create an override
* @param {GovernanceTarget} target
* @param {"allow"|"restrict"|"hide"|"deny"} visibility
* @param {string} [reason]
* @returns {Override}
*/
export function createOverride(target, visibility, reason) {
// Validate target
if (!target || typeof target !== "object") {
throw new Error("Target must be an object");
}
// Validate visibility
if (!["allow", "restrict", "hide", "deny"].includes(visibility)) {
throw new Error("Invalid visibility effect");
}
/** @type {Override} */
const override = {
target,
visibility
};
if (reason) {
override.reason = reason;
}
return override;
}
+104
View File
@@ -0,0 +1,104 @@
// Governance evidence for Nostr Governance
//
// Evidence explains a decision without leaking anything the application has not
// already resolved. Pubkeys are included because callers need them to render
// "muted by people you follow" affordances; display names and profile data are
// never resolved here.
/**
* @typedef {Object} EvidenceThresholds
* @property {number} [warn]
* @property {number} [restrict]
* @property {number} [hide]
* @property {number} [deny]
*/
/**
* @typedef {Object} GovernanceEvidence
* @property {number} trustedReportTotal
* @property {Record<string, number>} trustedReportsByCategory
* @property {string[]} trustedReporterPubkeys
* @property {number} trustedMuteTotal
* @property {Record<string, number>} trustedMutesByCategory
* @property {string[]} trustedMuterPubkeys
* @property {boolean} personalBlock
* @property {boolean} personalMute
* @property {boolean} userDenied
* @property {boolean} eventDenied
* @property {boolean} addressDenied
* @property {boolean} userAllowed
* @property {boolean} protectedTarget
* @property {EvidenceThresholds} thresholds
*/
/**
* Create an empty evidence record.
* @returns {GovernanceEvidence}
*/
export function createEmptyEvidence() {
return {
trustedReportTotal: 0,
trustedReportsByCategory: {},
trustedReporterPubkeys: [],
trustedMuteTotal: 0,
trustedMutesByCategory: {},
trustedMuterPubkeys: [],
personalBlock: false,
personalMute: false,
userDenied: false,
eventDenied: false,
addressDenied: false,
userAllowed: false,
protectedTarget: false,
thresholds: {},
};
}
/**
* Produce a stable, structurally-cloned copy of an evidence record.
*
* Pubkey lists and category maps are sorted so that two evaluations over the
* same snapshot serialize identically, which conformance fixtures depend on.
*
* @param {GovernanceEvidence} evidence
* @returns {GovernanceEvidence}
*/
export function freezeEvidence(evidence) {
/** @type {Record<string, number>} */
const reportsByCategory = {};
for (const key of Object.keys(evidence.trustedReportsByCategory).sort()) {
reportsByCategory[key] = evidence.trustedReportsByCategory[key];
}
/** @type {Record<string, number>} */
const mutesByCategory = {};
for (const key of Object.keys(evidence.trustedMutesByCategory).sort()) {
mutesByCategory[key] = evidence.trustedMutesByCategory[key];
}
return {
...evidence,
trustedReportsByCategory: reportsByCategory,
trustedMutesByCategory: mutesByCategory,
trustedReporterPubkeys: [...evidence.trustedReporterPubkeys].sort(),
trustedMuterPubkeys: [...evidence.trustedMuterPubkeys].sort(),
thresholds: { ...evidence.thresholds },
};
}
/**
* Strip evidence down to counts, dropping the pubkey lists.
*
* Used when a policy profile does not set `exposeEvidence`: the consumer still
* learns how strong the signal was, but not who produced it.
*
* @param {GovernanceEvidence} evidence
* @returns {GovernanceEvidence}
*/
export function redactEvidence(evidence) {
return {
...freezeEvidence(evidence),
trustedReporterPubkeys: [],
trustedMuterPubkeys: [],
};
}
+102
View File
@@ -0,0 +1,102 @@
// Snapshot fingerprints for Nostr Governance
//
// Fingerprints answer one question: "is this the same input as last time?"
// They drive cache invalidation and let a mismatch record point at which
// snapshot produced a decision. They are not a security primitive — a
// fingerprint collision misses a cache invalidation, it does not grant
// authority — so a fast pure-JS hash is used instead of WebCrypto, which would
// force the whole evaluation path to become async.
/**
* Canonical JSON: object keys sorted at every depth, so two structurally equal
* values always serialize to the same string.
* @param {unknown} value
* @returns {string}
*/
export function canonicalStringify(value) {
if (value === null || typeof value !== "object") {
return JSON.stringify(value) ?? "null";
}
if (Array.isArray(value)) {
return `[${value.map((entry) => canonicalStringify(entry)).join(",")}]`;
}
if (value instanceof Set) {
return `[${Array.from(value).map(String).sort().map((entry) => JSON.stringify(entry)).join(",")}]`;
}
if (value instanceof Map) {
const entries = Array.from(value.entries())
.map(([key, entry]) => [String(key), entry])
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalStringify(entry)}`).join(",")}}`;
}
const keys = Object.keys(/** @type {Record<string, unknown>} */ (value)).sort();
const parts = [];
for (const key of keys) {
const entry = /** @type {Record<string, unknown>} */ (value)[key];
if (entry === undefined) {
continue;
}
parts.push(`${JSON.stringify(key)}:${canonicalStringify(entry)}`);
}
return `{${parts.join(",")}}`;
}
/**
* FNV-1a over 32 bits, run twice with different offsets to widen the digest to
* 64 bits and keep accidental collisions unlikely for realistic snapshot counts.
* @param {string} input
* @returns {string} 16-character lowercase hex digest
*/
export function hashString(input) {
let high = 0x811c9dc5;
let low = 0x01000193;
for (let index = 0; index < input.length; index += 1) {
const code = input.charCodeAt(index);
high ^= code;
high = Math.imul(high, 0x01000193) >>> 0;
low ^= code + index;
low = Math.imul(low, 0x85ebca6b) >>> 0;
}
return high.toString(16).padStart(8, "0") + low.toString(16).padStart(8, "0");
}
/**
* Fingerprint any value.
* @param {unknown} value
* @returns {string}
*/
export function fingerprint(value) {
return hashString(canonicalStringify(value));
}
/**
* Fingerprint the parts of a governance snapshot that can change a decision.
*
* Deliberately excludes evaluation time and viewer identity: those belong to
* the evaluation, not the snapshot, and including them would defeat caching.
*
* @param {Object} snapshot
* @param {unknown} [snapshot.authority]
* @param {unknown} [snapshot.admin]
* @param {unknown} [snapshot.reports]
* @param {unknown} [snapshot.trustedMutes]
* @param {unknown} [snapshot.overrides]
* @param {unknown} [snapshot.policy]
* @returns {string}
*/
export function snapshotFingerprint(snapshot) {
return fingerprint({
authority: snapshot.authority ?? null,
admin: snapshot.admin ?? null,
reports: snapshot.reports ?? null,
trustedMutes: snapshot.trustedMutes ?? null,
overrides: snapshot.overrides ?? null,
policy: snapshot.policy ?? null,
});
}
+18 -2
View File
@@ -14,6 +14,8 @@
* @typedef {Object} EventTarget
* @property {"event"} type
* @property {string} id - 64-character hex event ID
* @property {string} [author] - Author pubkey, so author-level denial can reach the event
* @property {number} [kind] - Event kind, carried for application routing
*/
/**
@@ -134,16 +136,30 @@ export function createUserTarget(pubkey) {
/**
* Create an event target
* @param {string} id - Event ID in hex or note/nevent format
* @param {Object} [options]
* @param {string} [options.author] - Author pubkey; ignored when it is not valid hex
* @param {number} [options.kind] - Event kind
* @returns {EventTarget|null} Event target or null if invalid
*/
export function createEventTarget(id) {
export function createEventTarget(id, options = {}) {
const normalizedId = normalizeEventId(id);
if (!normalizedId) return null;
return {
/** @type {EventTarget} */
const target = {
type: "event",
id: normalizedId
};
const author = normalizePubkey(options.author ?? "");
if (author) {
target.author = author;
}
if (Number.isFinite(options.kind)) {
target.kind = /** @type {number} */ (options.kind);
}
return target;
}
/**
+118 -2
View File
@@ -1,2 +1,118 @@
// Placeholder for core module exports
export {};
// @nostr-governance/core
//
// Pure, headless governance primitives. No I/O, no browser globals, no
// application thresholds. Applications supply a policy definition and a
// snapshot; the evaluator returns a decision they apply.
export {
normalizePubkey,
normalizeEventId,
normalizeAddress,
createUserTarget,
createEventTarget,
getTargetKey,
} from "./identifiers.js";
export {
areTargetsEqual,
isValidTarget,
createTarget,
getParentTarget,
isDescendantOf,
} from "./targets.js";
export {
GOVERNANCE_CAPABILITIES,
DEFAULT_ROLE_CAPABILITIES,
isGovernanceCapability,
createAuthorityState,
getActorRoles,
getActorCapabilities,
hasCapability,
hasRole,
isProtectedActor,
createRoleDefinition,
} from "./authority.js";
export {
createEmptyAdminState,
reduceAdminState,
protectActors,
mergeCommunitySource,
isDenied,
serializeAdminState,
} from "./adminState.js";
export {
REASON_IDS,
isReasonId,
createReason,
dedupeReasons,
} from "./reasons.js";
export {
createEmptyEvidence,
freezeEvidence,
redactEvidence,
} from "./evidence.js";
export {
RANKING_LADDER,
VISIBILITY_LADDER,
INTERACTION_LADDER,
TRANSACTION_LADDER,
NEUTRAL_POLICY,
effectSeverity,
maxEffect,
createNeutralDecision,
normalizeDimensionEffects,
normalizeCategoryThresholds,
normalizePolicyProfile,
normalizePolicyDefinition,
createPolicyDefinition,
resolveProfile,
resolveThresholds,
applyThresholds,
applyEffects,
composeDecisions,
} from "./policy.js";
export {
canonicalStringify,
hashString,
fingerprint,
snapshotFingerprint,
} from "./fingerprint.js";
export {
evaluateTarget,
evaluateMany,
createSnapshot,
createViewerState,
resolveTrustSet,
aggregateReports,
aggregateMutes,
} from "./evaluator.js";
/**
* @typedef {import('./identifiers.js').GovernanceTarget} GovernanceTarget
* @typedef {import('./identifiers.js').UserTarget} UserTarget
* @typedef {import('./identifiers.js').EventTarget} EventTarget
* @typedef {import('./identifiers.js').AddressTarget} AddressTarget
* @typedef {import('./authority.js').AuthorityState} AuthorityState
* @typedef {import('./authority.js').GovernanceCapability} GovernanceCapability
* @typedef {import('./adminState.js').AdminState} AdminState
* @typedef {import('./adminState.js').Contribution} Contribution
* @typedef {import('./policy.js').PolicyDefinition} PolicyDefinition
* @typedef {import('./policy.js').PolicyProfile} PolicyProfile
* @typedef {import('./policy.js').PolicyContext} PolicyContext
* @typedef {import('./policy.js').GovernanceDecision} GovernanceDecision
* @typedef {import('./policy.js').CategoryThresholds} CategoryThresholds
* @typedef {import('./evidence.js').GovernanceEvidence} GovernanceEvidence
* @typedef {import('./reasons.js').GovernanceReason} GovernanceReason
* @typedef {import('./reasons.js').ReasonId} ReasonId
* @typedef {import('./evaluator.js').GovernanceSnapshot} GovernanceSnapshot
* @typedef {import('./evaluator.js').ViewerState} ViewerState
* @typedef {import('./evaluator.js').ReportRecord} ReportRecord
* @typedef {import('./evaluator.js').MuteRecord} MuteRecord
*/
+529 -320
View File
@@ -1,380 +1,589 @@
// Policy handling for Nostr Governance
// Policy engine for Nostr Governance
//
// This module handles policy definitions, decision making, and effect calculations
// for the governance system.
// The engine is deliberately generic: it carries no application thresholds of
// its own. Applications supply a policy definition made of named profiles, and
// each profile decides how evidence maps onto the four decision dimensions.
//
// Decision dimensions are independent ladders. A product can stay visible and
// inspectable while being downranked and blocked from checkout, without
// collapsing those states into a single "hidden" boolean.
import { createReason, dedupeReasons } from "./reasons.js";
/**
* @typedef {Object} PolicyEffect
* @property {"allow"|"restrict"|"hide"|"deny"} effect - The base effect
* @property {string} [reason] - Reason for the effect
* @property {any} [details] - Additional details about the effect
* @typedef {import('./reasons.js').GovernanceReason} GovernanceReason
* @typedef {import('./identifiers.js').GovernanceTarget} GovernanceTarget
* @typedef {import('./evidence.js').GovernanceEvidence} GovernanceEvidence
*/
/**
* @typedef {"normal"|"downrank"} RankingEffect
* @typedef {"allow"|"warn"|"restrict"|"hide"|"deny"} VisibilityEffect
* @typedef {"allow"|"require-explicit-action"|"deny"} InteractionEffect
* @typedef {"allow"|"require-review"|"deny"} TransactionEffect
*/
/**
* Severity ladders. Composition always keeps the most severe effect seen, so
* these arrays define the entire merge semantics for each dimension.
*/
export const RANKING_LADDER = Object.freeze(["normal", "downrank"]);
export const VISIBILITY_LADDER = Object.freeze(["allow", "warn", "restrict", "hide", "deny"]);
export const INTERACTION_LADDER = Object.freeze(["allow", "require-explicit-action", "deny"]);
export const TRANSACTION_LADDER = Object.freeze(["allow", "require-review", "deny"]);
/** @type {Record<string, readonly string[]>} */
const LADDERS = {
ranking: RANKING_LADDER,
visibility: VISIBILITY_LADDER,
interaction: INTERACTION_LADDER,
transaction: TRANSACTION_LADDER,
};
/**
* Rank an effect within its dimension's ladder.
* @param {"ranking"|"visibility"|"interaction"|"transaction"} dimension
* @param {string} effect
* @returns {number} Ladder index, or -1 when the effect is not valid
*/
export function effectSeverity(dimension, effect) {
const ladder = LADDERS[dimension];
if (!ladder) {
throw new Error(`Unknown decision dimension: ${dimension}`);
}
return ladder.indexOf(effect);
}
/**
* Return the more severe of two effects in the same dimension.
* @param {"ranking"|"visibility"|"interaction"|"transaction"} dimension
* @param {string} a
* @param {string} b
* @returns {string}
*/
export function maxEffect(dimension, a, b) {
const severityA = effectSeverity(dimension, a);
const severityB = effectSeverity(dimension, b);
if (severityA < 0) return b;
if (severityB < 0) return a;
return severityA >= severityB ? a : b;
}
/**
* @typedef {Object} CategoryThresholds
* @property {number} [downrank] - Count at which the target is downranked
* @property {number} [warn] - Count at which the viewer is warned
* @property {number} [restrict] - Count at which visibility is restricted
* @property {number} [hide] - Count at which the target is hidden
* @property {number} [deny] - Count at which the target is denied outright
* @property {number} [interactionDeny] - Count at which interaction is denied
* @property {number} [requireExplicitAction] - Count at which interaction needs explicit intent
* @property {number} [transactionReview] - Count at which a transaction needs review
* @property {number} [transactionDeny] - Count at which a transaction is denied
*/
/**
* @typedef {Object} DimensionEffects
* @property {RankingEffect} [ranking]
* @property {VisibilityEffect} [visibility]
* @property {InteractionEffect} [interaction]
* @property {TransactionEffect} [transaction]
*/
/**
* @typedef {Object} PolicyProfile
* @property {string} name - Profile name
* @property {Record<string, PolicyEffect>} effects - Map of effect types to policy effects
* @property {string} name
* @property {DimensionEffects} [administrativeDeny] - Applied on admin/community denial
* @property {DimensionEffects} [viewerBlock] - Applied when the viewer personally blocks
* @property {DimensionEffects} [allowlistMiss] - Applied when allowlist mode is on and the target misses
* @property {boolean} [requireAllowlist] - Whether allowlist misses are enforced on this surface
* @property {boolean} [allowViewerOverride] - Whether viewer overrides may soften a decision
* @property {boolean} [exposeEvidence] - Whether the consumer may surface evidence detail
* @property {boolean} [bypassHide] - Surface exception: cap visibility below `hide`
* @property {VisibilityEffect} [bypassHideCeiling] - Effect used when `bypassHide` caps a hide
* @property {boolean} [disabled] - Disable governance entirely for this surface
* @property {Record<string, CategoryThresholds>} [reports] - Report thresholds by category ("default" applies to unlisted categories)
* @property {Record<string, CategoryThresholds>} [mutes] - Trusted-mute thresholds by category ("default" applies to totals)
* @property {number} [muteWindowSeconds] - Trusted mutes older than this are ignored (0 disables expiry)
*/
/**
* @typedef {Object} PolicyDefinition
* @property {string} id - Unique policy identifier
* @property {string} name - Human-readable name
* @property {string} description - Description of what the policy does
* @property {Record<string, PolicyProfile>} profiles - Map of profile names to policy profiles
* @property {string} id
* @property {string} [name] - Defaults to the id when omitted
* @property {string} [description]
* @property {string} version - Policy version, reported on every decision
* @property {Record<string, PolicyProfile>} profiles
* @property {string} [defaultProfile]
*/
/**
* @typedef {Object} PolicyContext
* @property {string} surface - The surface where the policy is being applied (e.g., "feed", "search", "checkout")
* @property {string} policyProfile - The policy profile to use
* @property {Object} [enforcement] - Enforcement options
* @property {boolean} [enforcement.hardHide] - Whether to hard hide denied items
* @property {boolean} [enforcement.allowOverrides] - Whether to allow viewer overrides
* @property {string} surface - Surface being rendered (e.g. "feed", "checkout")
* @property {string} [policyProfile] - Profile name; defaults to the policy's default profile
* @property {PolicyDefinition} [policy] - Policy definition to evaluate against
* @property {number} [now] - Evaluation time in unix seconds (injected clock)
* @property {Object} [enforcement]
* @property {boolean} [enforcement.allowOverrides] - Hard switch for viewer overrides
*/
/**
* @typedef {Object} DecisionDimensions
* @property {{ effect: RankingEffect, weight: number }} ranking
* @property {{ effect: VisibilityEffect, overridable: boolean }} visibility
* @property {{ effect: InteractionEffect }} interaction
* @property {{ effect: TransactionEffect }} [transaction]
*/
/**
* @typedef {Object} GovernanceDecision
* @property {PolicyEffect} visibility - Visibility effect
* @property {PolicyEffect} interaction - Interaction effect
* @property {PolicyEffect} [transaction] - Transaction effect (for commerce)
* @property {string[]} reasons - Array of reasons for the decision
* @property {any[]} evidence - Array of evidence supporting the decision
* @property {Object} [metadata] - Additional metadata about the decision
* @property {GovernanceTarget} [target]
* @property {string} key
* @property {{ effect: RankingEffect, weight: number }} ranking
* @property {{ effect: VisibilityEffect, overridable: boolean }} visibility
* @property {{ effect: InteractionEffect }} interaction
* @property {{ effect: TransactionEffect }} [transaction]
* @property {GovernanceReason[]} reasons
* @property {GovernanceEvidence} [evidence]
* @property {string} policyProfile
* @property {string} policyVersion
* @property {string} snapshotFingerprint
* @property {number} evaluatedAt
*/
/**
* Built-in policy profiles
* @type {Record<string, PolicyProfile>}
*/
export const BUILTIN_POLICY_PROFILES = {
"default": {
name: "default",
effects: {
"visibility": { effect: "allow" },
"interaction": { effect: "allow" }
}
},
"moderate": {
name: "moderate",
effects: {
"visibility": { effect: "restrict" },
"interaction": { effect: "allow" }
}
},
"strict": {
name: "strict",
effects: {
"visibility": { effect: "hide" },
"interaction": { effect: "deny" }
}
},
"commerce-default": {
name: "commerce-default",
effects: {
"visibility": { effect: "allow" },
"interaction": { effect: "allow" },
"transaction": { effect: "allow" }
}
},
"commerce-transaction": {
name: "commerce-transaction",
effects: {
"visibility": { effect: "allow" },
"interaction": { effect: "allow" },
"transaction": { effect: "deny" }
}
}
};
/**
* Create a policy effect
* @param {"allow"|"restrict"|"hide"|"deny"} effect
* @param {string} [reason]
* @param {any} [details]
* @returns {PolicyEffect}
*/
export function createPolicyEffect(effect, reason, details) {
if (!["allow", "restrict", "hide", "deny"].includes(effect)) {
throw new Error("Invalid policy effect: " + effect);
}
const policyEffect = { effect };
if (reason) {
policyEffect.reason = reason;
}
if (details) {
policyEffect.details = details;
}
return policyEffect;
}
/**
* Create a policy profile
* @param {string} name
* @param {Record<string, PolicyEffect>} effects
* @returns {PolicyProfile}
*/
export function createPolicyProfile(name, effects) {
if (typeof name !== "string" || !name.trim()) {
throw new Error("Policy profile name must be a non-empty string");
}
if (!effects || typeof effects !== "object") {
throw new Error("Policy profile effects must be an object");
}
// Validate each effect
for (const [effectType, effect] of Object.entries(effects)) {
if (typeof effectType !== "string" || !effectType.trim()) {
throw new Error("Effect types must be non-empty strings");
}
if (!effect || typeof effect !== "object") {
throw new Error("Effects must be objects");
}
if (!["allow", "restrict", "hide", "deny"].includes(effect.effect)) {
throw new Error("Invalid policy effect: " + effect.effect);
}
}
return {
name: name.trim(),
effects
};
}
/**
* Create a policy definition
* @param {string} id
* @param {string} name
* @param {string} description
* @param {Record<string, PolicyProfile>} profiles
* @returns {PolicyDefinition}
*/
export function createPolicyDefinition(id, name, description, profiles) {
if (typeof id !== "string" || !id.trim()) {
throw new Error("Policy ID must be a non-empty string");
}
if (typeof name !== "string" || !name.trim()) {
throw new Error("Policy name must be a non-empty string");
}
if (typeof description !== "string") {
throw new Error("Policy description must be a string");
}
if (!profiles || typeof profiles !== "object") {
throw new Error("Policy profiles must be an object");
}
// Validate each profile
for (const [profileName, profile] of Object.entries(profiles)) {
if (typeof profileName !== "string" || !profileName.trim()) {
throw new Error("Profile names must be non-empty strings");
}
if (!profile || typeof profile !== "object") {
throw new Error("Profiles must be objects");
}
if (profile.name !== profileName) {
throw new Error(`Profile name mismatch: ${profileName} !== ${profile.name}`);
}
}
return {
id: id.trim(),
name: name.trim(),
description,
profiles
};
}
/**
* Create a policy context
* @param {string} surface
* @param {string} policyProfile
* @param {Object} [enforcement]
* @returns {PolicyContext}
*/
export function createPolicyContext(surface, policyProfile, enforcement) {
if (typeof surface !== "string" || !surface.trim()) {
throw new Error("Surface must be a non-empty string");
}
if (typeof policyProfile !== "string" || !policyProfile.trim()) {
throw new Error("Policy profile must be a non-empty string");
}
const context = {
surface: surface.trim(),
policyProfile: policyProfile.trim()
};
if (enforcement) {
context.enforcement = {};
if (typeof enforcement.hardHide === "boolean") {
context.enforcement.hardHide = enforcement.hardHide;
}
if (typeof enforcement.allowOverrides === "boolean") {
context.enforcement.allowOverrides = enforcement.allowOverrides;
}
}
return context;
}
/**
* Create a governance decision
* @param {PolicyEffect} visibility
* @param {PolicyEffect} interaction
* @param {PolicyEffect} [transaction]
* @param {string[]} [reasons]
* @param {any[]} [evidence]
* @param {Object} [metadata]
* A decision with every dimension at its most permissive value.
* @param {Object} [options]
* @param {string} [options.key]
* @param {GovernanceTarget} [options.target]
* @param {boolean} [options.includeTransaction]
* @returns {GovernanceDecision}
*/
export function createGovernanceDecision(visibility, interaction, transaction, reasons, evidence, metadata) {
// Validate visibility effect
if (!visibility || typeof visibility !== "object" ||
!["allow", "restrict", "hide", "deny"].includes(visibility.effect)) {
throw new Error("Invalid visibility effect");
}
// Validate interaction effect
if (!interaction || typeof interaction !== "object" ||
!["allow", "restrict", "hide", "deny"].includes(interaction.effect)) {
throw new Error("Invalid interaction effect");
}
// Validate transaction effect if provided
if (transaction && (typeof transaction !== "object" ||
!["allow", "restrict", "hide", "deny"].includes(transaction.effect))) {
throw new Error("Invalid transaction effect");
}
// Validate reasons
if (reasons && !Array.isArray(reasons)) {
throw new Error("Reasons must be an array");
}
// Validate evidence
if (evidence && !Array.isArray(evidence)) {
throw new Error("Evidence must be an array");
}
export function createNeutralDecision(options = {}) {
/** @type {GovernanceDecision} */
const decision = {
visibility,
interaction,
reasons: reasons || [],
evidence: evidence || []
key: options.key ?? "",
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
reasons: [],
policyProfile: "",
policyVersion: "",
snapshotFingerprint: "",
evaluatedAt: 0,
};
if (transaction) {
decision.transaction = transaction;
if (options.target) {
decision.target = options.target;
}
if (metadata) {
decision.metadata = metadata;
if (options.includeTransaction) {
decision.transaction = { effect: "allow" };
}
return decision;
}
/**
* Get effect for a policy profile
* @param {string} profileName
* @param {string} effectType
* @param {Record<string, PolicyDefinition>} [customPolicies]
* @returns {PolicyEffect}
* Validate a set of dimension effects supplied by an application profile.
* @param {DimensionEffects} [effects]
* @param {string} location - Human-readable location for error messages
* @returns {DimensionEffects}
*/
export function getPolicyEffect(profileName, effectType, customPolicies) {
// Check custom policies first
if (customPolicies) {
for (const policy of Object.values(customPolicies)) {
if (policy.profiles[profileName] && policy.profiles[profileName].effects[effectType]) {
return policy.profiles[profileName].effects[effectType];
}
}
export function normalizeDimensionEffects(effects, location = "profile") {
if (!effects || typeof effects !== "object") {
return {};
}
// Check built-in profiles
if (BUILTIN_POLICY_PROFILES[profileName] &&
BUILTIN_POLICY_PROFILES[profileName].effects[effectType]) {
return BUILTIN_POLICY_PROFILES[profileName].effects[effectType];
/** @type {DimensionEffects} */
const normalized = {};
for (const dimension of /** @type {const} */ ([
"ranking",
"visibility",
"interaction",
"transaction",
])) {
const value = effects[dimension];
if (value === undefined) {
continue;
}
if (effectSeverity(dimension, value) < 0) {
throw new Error(`${location}: invalid ${dimension} effect "${String(value)}"`);
}
// @ts-expect-error -- the ladder check above narrows the value for us
normalized[dimension] = value;
}
// Default to allow
return { effect: "allow" };
return normalized;
}
/**
* Compose multiple governance decisions into a single decision
* @param {GovernanceDecision[]} decisions
* @returns {GovernanceDecision}
* Normalize category thresholds, dropping non-numeric entries.
* @param {Record<string, CategoryThresholds>} [thresholds]
* @param {string} [location]
* @returns {Record<string, CategoryThresholds>}
*/
export function composeGovernanceDecisions(decisions) {
if (!Array.isArray(decisions) || decisions.length === 0) {
// Return a default allow decision
return createGovernanceDecision(
{ effect: "allow" },
{ effect: "allow" },
undefined,
[],
[]
export function normalizeCategoryThresholds(thresholds, location = "profile") {
if (!thresholds || typeof thresholds !== "object") {
return {};
}
/** @type {Record<string, CategoryThresholds>} */
const normalized = {};
for (const [category, config] of Object.entries(thresholds)) {
if (!config || typeof config !== "object") {
continue;
}
const trimmed = category.trim().toLowerCase();
if (!trimmed) {
continue;
}
/** @type {CategoryThresholds} */
const entry = {};
for (const [gate, value] of Object.entries(config)) {
if (!Number.isFinite(value)) {
continue;
}
if (/** @type {number} */ (value) < 0) {
throw new Error(`${location}: threshold "${gate}" for "${category}" must be >= 0`);
}
// Gate names are validated where thresholds are applied; unknown keys are
// carried through harmlessly rather than rejected here.
/** @type {Record<string, number>} */ (entry)[gate] = Math.floor(
/** @type {number} */ (value),
);
}
normalized[trimmed] = entry;
}
return normalized;
}
/**
* Normalize a policy profile.
* @param {PolicyProfile} profile
* @returns {PolicyProfile}
*/
export function normalizePolicyProfile(profile) {
if (!profile || typeof profile !== "object") {
throw new Error("Policy profile must be an object");
}
if (typeof profile.name !== "string" || !profile.name.trim()) {
throw new Error("Policy profile requires a non-empty name");
}
const name = profile.name.trim();
const location = `profile "${name}"`;
/** @type {PolicyProfile} */
const normalized = {
name,
administrativeDeny: normalizeDimensionEffects(profile.administrativeDeny, location),
viewerBlock: normalizeDimensionEffects(profile.viewerBlock, location),
allowlistMiss: normalizeDimensionEffects(profile.allowlistMiss, location),
requireAllowlist: profile.requireAllowlist === true,
allowViewerOverride: profile.allowViewerOverride !== false,
exposeEvidence: profile.exposeEvidence === true,
bypassHide: profile.bypassHide === true,
disabled: profile.disabled === true,
reports: normalizeCategoryThresholds(profile.reports, location),
mutes: normalizeCategoryThresholds(profile.mutes, location),
};
if (Number.isFinite(profile.muteWindowSeconds)) {
const window = /** @type {number} */ (profile.muteWindowSeconds);
if (window < 0) {
throw new Error(`${location}: muteWindowSeconds must be >= 0`);
}
normalized.muteWindowSeconds = Math.floor(window);
}
if (profile.bypassHideCeiling !== undefined) {
if (effectSeverity("visibility", profile.bypassHideCeiling) < 0) {
throw new Error(`${location}: invalid bypassHideCeiling "${String(profile.bypassHideCeiling)}"`);
}
normalized.bypassHideCeiling = profile.bypassHideCeiling;
} else if (normalized.bypassHide) {
normalized.bypassHideCeiling = "restrict";
}
return normalized;
}
/**
* Normalize a full policy definition.
* @param {PolicyDefinition} policy
* @returns {PolicyDefinition}
*/
export function normalizePolicyDefinition(policy) {
if (!policy || typeof policy !== "object") {
throw new Error("Policy definition must be an object");
}
if (typeof policy.id !== "string" || !policy.id.trim()) {
throw new Error("Policy definition requires a non-empty id");
}
if (typeof policy.version !== "string" || !policy.version.trim()) {
throw new Error("Policy definition requires a version string");
}
if (!policy.profiles || typeof policy.profiles !== "object") {
throw new Error("Policy definition requires a profiles map");
}
/** @type {Record<string, PolicyProfile>} */
const profiles = {};
for (const [key, profile] of Object.entries(policy.profiles)) {
const normalizedProfile = normalizePolicyProfile({ ...profile, name: profile?.name ?? key });
profiles[key.trim()] = normalizedProfile;
}
const profileNames = Object.keys(profiles);
if (profileNames.length === 0) {
throw new Error("Policy definition requires at least one profile");
}
const defaultProfile = policy.defaultProfile?.trim() || profileNames[0];
if (!profiles[defaultProfile]) {
throw new Error(`Policy default profile "${defaultProfile}" is not defined`);
}
return {
id: policy.id.trim(),
name: typeof policy.name === "string" && policy.name.trim() ? policy.name.trim() : policy.id.trim(),
description: typeof policy.description === "string" ? policy.description : "",
version: policy.version.trim(),
profiles,
defaultProfile,
};
}
/**
* Resolve the profile a context selects.
* @param {PolicyDefinition} policy
* @param {PolicyContext} [context]
* @returns {PolicyProfile}
*/
export function resolveProfile(policy, context) {
const requested = context?.policyProfile?.trim();
if (requested) {
const profile = policy.profiles[requested];
if (!profile) {
throw new Error(`Unknown policy profile: ${requested}`);
}
return profile;
}
const fallback = policy.defaultProfile ?? Object.keys(policy.profiles)[0];
return policy.profiles[fallback];
}
/**
* Resolve thresholds for a category, falling back to the profile "default" entry.
* @param {Record<string, CategoryThresholds>} [table]
* @param {string} [category]
* @returns {CategoryThresholds}
*/
export function resolveThresholds(table, category) {
if (!table) {
return {};
}
const normalizedCategory = typeof category === "string" ? category.trim().toLowerCase() : "";
if (normalizedCategory && table[normalizedCategory]) {
return table[normalizedCategory];
}
return table.default ?? {};
}
/**
* Map a count against category thresholds into dimension effects.
*
* A gate fires when `count >= threshold`. A threshold of 0 is treated as
* "never fires" so that an application can disable a gate by zeroing it rather
* than having it trigger on every target.
*
* @param {number} count
* @param {CategoryThresholds} thresholds
* @returns {{ effects: DimensionEffects, firedGates: string[] }}
*/
export function applyThresholds(count, thresholds) {
/** @type {DimensionEffects} */
const effects = {};
/** @type {string[]} */
const firedGates = [];
if (!Number.isFinite(count) || count <= 0) {
return { effects, firedGates };
}
/** @type {Array<[keyof CategoryThresholds, "ranking"|"visibility"|"interaction"|"transaction", string]>} */
const gates = [
["downrank", "ranking", "downrank"],
["warn", "visibility", "warn"],
["restrict", "visibility", "restrict"],
["hide", "visibility", "hide"],
["deny", "visibility", "deny"],
["requireExplicitAction", "interaction", "require-explicit-action"],
["interactionDeny", "interaction", "deny"],
["transactionReview", "transaction", "require-review"],
["transactionDeny", "transaction", "deny"],
];
for (const [gate, dimension, effect] of gates) {
const threshold = thresholds[gate];
if (!Number.isFinite(threshold) || /** @type {number} */ (threshold) <= 0) {
continue;
}
if (count < /** @type {number} */ (threshold)) {
continue;
}
const current = effects[dimension];
// @ts-expect-error -- effect strings are drawn from the matching ladder
effects[dimension] = current ? maxEffect(dimension, current, effect) : effect;
firedGates.push(String(gate));
}
return { effects, firedGates };
}
/**
* Apply dimension effects onto a decision, keeping the most severe value.
* @param {GovernanceDecision} decision
* @param {DimensionEffects} effects
* @param {GovernanceReason[]} [reasons]
* @returns {GovernanceDecision} The same decision, mutated in place
*/
export function applyEffects(decision, effects, reasons = []) {
if (effects.ranking) {
const next = maxEffect("ranking", decision.ranking.effect, effects.ranking);
if (next !== decision.ranking.effect) {
decision.ranking.effect = /** @type {RankingEffect} */ (next);
}
if (effects.ranking === "downrank") {
decision.ranking.weight += 1;
}
}
if (effects.visibility) {
decision.visibility.effect = /** @type {VisibilityEffect} */ (
maxEffect("visibility", decision.visibility.effect, effects.visibility)
);
}
// Start with the first decision
const composed = { ...decisions[0] };
// For each subsequent decision, apply the most restrictive effects
for (let i = 1; i < decisions.length; i++) {
const decision = decisions[i];
// Compose visibility effect (hide > restrict > deny > allow)
const visibilityOrder = { "allow": 0, "restrict": 1, "hide": 2, "deny": 3 };
if (visibilityOrder[decision.visibility.effect] > visibilityOrder[composed.visibility.effect]) {
composed.visibility = decision.visibility;
if (effects.interaction) {
decision.interaction.effect = /** @type {InteractionEffect} */ (
maxEffect("interaction", decision.interaction.effect, effects.interaction)
);
}
// Compose interaction effect (deny > hide > restrict > allow)
const interactionOrder = { "allow": 0, "restrict": 1, "hide": 2, "deny": 3 };
if (interactionOrder[decision.interaction.effect] > interactionOrder[composed.interaction.effect]) {
composed.interaction = decision.interaction;
if (effects.transaction) {
if (!decision.transaction) {
decision.transaction = { effect: "allow" };
}
decision.transaction.effect = /** @type {TransactionEffect} */ (
maxEffect("transaction", decision.transaction.effect, effects.transaction)
);
}
// Compose transaction effect if present
if (reasons.length) {
decision.reasons = dedupeReasons([...decision.reasons, ...reasons]);
}
return decision;
}
/**
* Compose several decisions into one, keeping the most severe effect per
* dimension and the union of reasons.
*
* Composition is commutative and associative: every dimension merges by ladder
* maximum, so the order decisions arrive in cannot change the result.
*
* @param {GovernanceDecision[]} decisions
* @returns {GovernanceDecision}
*/
export function composeDecisions(decisions) {
if (!Array.isArray(decisions) || decisions.length === 0) {
return createNeutralDecision();
}
const [first, ...rest] = decisions;
/** @type {GovernanceDecision} */
const composed = {
...first,
ranking: { ...first.ranking },
visibility: { ...first.visibility },
interaction: { ...first.interaction },
reasons: [...first.reasons],
};
if (first.transaction) {
composed.transaction = { ...first.transaction };
}
for (const decision of rest) {
composed.ranking.effect = /** @type {RankingEffect} */ (
maxEffect("ranking", composed.ranking.effect, decision.ranking.effect)
);
composed.ranking.weight += decision.ranking.weight;
composed.visibility.effect = /** @type {VisibilityEffect} */ (
maxEffect("visibility", composed.visibility.effect, decision.visibility.effect)
);
composed.visibility.overridable = composed.visibility.overridable && decision.visibility.overridable;
composed.interaction.effect = /** @type {InteractionEffect} */ (
maxEffect("interaction", composed.interaction.effect, decision.interaction.effect)
);
if (decision.transaction) {
if (!composed.transaction) {
composed.transaction = decision.transaction;
composed.transaction = { ...decision.transaction };
} else {
const transactionOrder = { "allow": 0, "restrict": 1, "hide": 2, "deny": 3 };
if (transactionOrder[decision.transaction.effect] > transactionOrder[composed.transaction.effect]) {
composed.transaction = decision.transaction;
}
}
}
// Combine reasons and evidence
for (const reason of decision.reasons) {
if (!composed.reasons.includes(reason)) {
composed.reasons.push(reason);
composed.transaction.effect = /** @type {TransactionEffect} */ (
maxEffect("transaction", composed.transaction.effect, decision.transaction.effect)
);
}
}
for (const evidence of decision.evidence) {
composed.evidence.push(evidence);
}
composed.reasons = [...composed.reasons, ...decision.reasons];
}
composed.reasons = dedupeReasons(composed.reasons);
return composed;
}
/**
* Create a policy definition, validating and normalizing it.
* @param {PolicyDefinition} policy
* @returns {PolicyDefinition}
*/
export function createPolicyDefinition(policy) {
return normalizePolicyDefinition(policy);
}
/**
* A permissive policy used when an application supplies none. It enforces
* administrative denials but applies no trust thresholds of its own, matching
* the rule that the generic engine never hides content merely because one
* trusted account acted against it.
* @type {PolicyDefinition}
*/
export const NEUTRAL_POLICY = normalizePolicyDefinition({
id: "neutral",
name: "Neutral",
description: "Enforces administrative denials only. Applications should supply their own policy.",
version: "1.0.0",
defaultProfile: "default",
profiles: {
default: {
name: "default",
administrativeDeny: { visibility: "hide", interaction: "deny", transaction: "deny" },
viewerBlock: { visibility: "hide", interaction: "deny" },
allowlistMiss: { visibility: "hide", interaction: "deny" },
allowViewerOverride: true,
reports: {},
mutes: {},
},
},
});
export { createReason };
+135
View File
@@ -0,0 +1,135 @@
// Stable reason identifiers for Nostr Governance
//
// Reason identifiers are part of the public contract. Applications map these
// identifiers to their own wording; the engine never emits user-facing prose.
// Adding a new identifier is a minor change. Renaming or removing one is a
// breaking change.
/**
* @typedef {"viewer-block"
* | "viewer-mute"
* | "viewer-override"
* | "admin-user-deny"
* | "admin-event-deny"
* | "admin-address-deny"
* | "community-user-deny"
* | "trusted-report"
* | "trusted-report-threshold"
* | "trusted-mute"
* | "trusted-mute-threshold"
* | "allowlist-miss"
* | "protected-target"
* | "surface-policy-bypass"
* | "policy-disabled"} ReasonId
*/
/**
* Every stable reason identifier the evaluator may emit.
* @type {readonly ReasonId[]}
*/
export const REASON_IDS = Object.freeze([
"viewer-block",
"viewer-mute",
"viewer-override",
"admin-user-deny",
"admin-event-deny",
"admin-address-deny",
"community-user-deny",
"trusted-report",
"trusted-report-threshold",
"trusted-mute",
"trusted-mute-threshold",
"allowlist-miss",
"protected-target",
"surface-policy-bypass",
"policy-disabled",
]);
const REASON_ID_SET = new Set(REASON_IDS);
/**
* @typedef {Object} GovernanceReason
* @property {ReasonId} id - Stable identifier
* @property {string} [category] - Report or mute category the reason relates to
* @property {number} [count] - Observed count that produced the reason
* @property {number} [threshold] - Threshold the count was compared against
* @property {string} [source] - Opaque origin marker (e.g. a curator pubkey)
*/
/**
* Check whether a value is a known reason identifier.
* @param {unknown} value
* @returns {value is ReasonId}
*/
export function isReasonId(value) {
return typeof value === "string" && REASON_ID_SET.has(/** @type {ReasonId} */ (value));
}
/**
* Create a governance reason.
* @param {ReasonId} id
* @param {Object} [detail]
* @param {string} [detail.category]
* @param {number} [detail.count]
* @param {number} [detail.threshold]
* @param {string} [detail.source]
* @returns {GovernanceReason}
*/
export function createReason(id, detail = {}) {
if (!isReasonId(id)) {
throw new Error(`Unknown reason identifier: ${String(id)}`);
}
/** @type {GovernanceReason} */
const reason = { id };
if (typeof detail.category === "string" && detail.category.trim()) {
reason.category = detail.category.trim();
}
if (Number.isFinite(detail.count)) {
reason.count = /** @type {number} */ (detail.count);
}
if (Number.isFinite(detail.threshold)) {
reason.threshold = /** @type {number} */ (detail.threshold);
}
if (typeof detail.source === "string" && detail.source.trim()) {
reason.source = detail.source.trim();
}
return reason;
}
/**
* Deduplicate reasons, preserving first-seen order.
*
* Two reasons are the same when their identifier, category, and threshold all
* match; counts are allowed to differ and the highest is kept so that a merged
* decision reports the strongest observed evidence.
*
* @param {GovernanceReason[]} reasons
* @returns {GovernanceReason[]}
*/
export function dedupeReasons(reasons) {
/** @type {Map<string, GovernanceReason>} */
const byKey = new Map();
for (const reason of reasons) {
if (!reason || !isReasonId(reason.id)) {
continue;
}
const key = `${reason.id}|${reason.category ?? ""}|${reason.threshold ?? ""}`;
const existing = byKey.get(key);
if (!existing) {
byKey.set(key, { ...reason });
continue;
}
if (Number.isFinite(reason.count)) {
const nextCount = /** @type {number} */ (reason.count);
if (!Number.isFinite(existing.count) || nextCount > /** @type {number} */ (existing.count)) {
existing.count = nextCount;
}
}
}
return Array.from(byKey.values());
}
+12 -6
View File
@@ -56,7 +56,7 @@ export function isValidTarget(target) {
/**
* Create a target from various input formats
* @param {string|{type: string, pubkey?: string, id?: string, kind?: string|number, identifier?: string}|null|undefined} input
* @param {string|{type: string, pubkey?: string, id?: string, author?: string, kind?: string|number, identifier?: string}|null|undefined} input
* @returns {GovernanceTarget|null}
*/
export function createTarget(input) {
@@ -116,7 +116,10 @@ export function createTarget(input) {
case "user":
return createUserTarget(/** @type {string} */ (input.pubkey));
case "event":
return createEventTarget(/** @type {string} */ (input.id));
return createEventTarget(/** @type {string} */ (input.id), {
author: /** @type {string|undefined} */ (input.author),
kind: typeof input.kind === "number" ? input.kind : undefined,
});
case "address":
return normalizeAddress(
/** @type {string|number} */ (input.kind),
@@ -145,12 +148,15 @@ export function getParentTarget(target, authorPubkey) {
case "user":
// User targets have no parent
return null;
case "event":
// Event targets have the author as parent, but we need the author pubkey
if (!authorPubkey) {
case "event": {
// Event targets have the author as parent. The author may be carried on
// the target itself; the explicit argument stays supported and wins.
const author = authorPubkey ?? target.author;
if (!author) {
throw new Error("authorPubkey is required for event targets");
}
return createUserTarget(authorPubkey);
return createUserTarget(author);
}
case "address":
// Address targets have the author as parent
return createUserTarget(target.pubkey);
+233
View File
@@ -0,0 +1,233 @@
import { describe, expect, it } from "vitest";
import { createAuthorityState } from "../src/authority.js";
import {
createEmptyAdminState,
isDenied,
mergeCommunitySource,
reduceAdminState,
serializeAdminState,
} from "../src/adminState.js";
const ROOT = "a1".repeat(32);
const MODERATOR = "b2".repeat(32);
const CURATOR = "c3".repeat(32);
const CREATOR = "d4".repeat(32);
const STRANGER = "e5".repeat(32);
const EVENT_ID = "1b".repeat(32);
const authority = () =>
createAuthorityState({
root: ROOT,
actors: {
[ROOT]: ["super_admin"],
[MODERATOR]: ["moderator"],
[CURATOR]: ["curator"],
},
});
/** @param {string} pubkey @returns {import("../src/identifiers.js").UserTarget} */
const user = (pubkey) => ({ type: "user", pubkey });
/** @param {string} id @returns {import("../src/identifiers.js").EventTarget} */
const event = (id) => ({ type: "event", id });
/**
* @param {string} kind
* @param {string} pubkey
* @param {string} identifier
* @returns {import("../src/identifiers.js").AddressTarget}
*/
const address = (kind, pubkey, identifier) => ({ type: "address", kind, pubkey, identifier });
describe("reduceAdminState", () => {
it("accepts a contribution the actor is authorized to make", () => {
const state = reduceAdminState(
[{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] }],
authority(),
);
expect(state.userDeny.has(`user:${CREATOR}`)).toBe(true);
});
it("drops a contribution beyond the actor's capabilities", () => {
const state = reduceAdminState(
[{ actor: CURATOR, kind: "event-deny", targets: [event(EVENT_ID)] }],
authority(),
);
expect(state.eventDeny.size).toBe(0);
});
it("drops contributions from actors with no roles", () => {
const state = reduceAdminState(
[{ actor: STRANGER, kind: "user-deny", targets: [user(CREATOR)] }],
authority(),
);
expect(state.userDeny.size).toBe(0);
});
it("drops an actor's entries as soon as the role is revoked", () => {
/** @type {import("../src/adminState.js").Contribution[]} */
const contributions = [{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] }];
const withRole = reduceAdminState(contributions, authority());
expect(withRole.userDeny.size).toBe(1);
const revoked = createAuthorityState({ root: ROOT, actors: { [ROOT]: ["super_admin"] } });
expect(reduceAdminState(contributions, revoked).userDeny.size).toBe(0);
});
it("unions denials from several contributors", () => {
const state = reduceAdminState(
[
{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] },
{ actor: CURATOR, kind: "user-deny", targets: [user(CREATOR)] },
],
authority(),
);
expect(state.contributors.get(`user:${CREATOR}`)).toEqual([MODERATOR, CURATOR]);
});
it("ignores targets of the wrong type for the contribution kind", () => {
const state = reduceAdminState(
[{ actor: MODERATOR, kind: "event-deny", targets: [user(CREATOR)] }],
authority(),
);
expect(state.eventDeny.size).toBe(0);
});
it("ignores malformed contributions and targets", () => {
const state = reduceAdminState(
/** @type {any[]} */ ([
null,
{ actor: MODERATOR, kind: "not-a-kind", targets: [user(CREATOR)] },
{ actor: MODERATOR, kind: "user-deny", targets: [/** @type {any} */ ({ type: "user", pubkey: "short" })] },
{ actor: MODERATOR, kind: "user-deny" },
]),
authority(),
);
expect(state.userDeny.size).toBe(0);
});
it("collects trust seeds as pubkeys", () => {
const state = reduceAdminState(
[{ actor: MODERATOR, kind: "trust-seed", targets: [user(CREATOR)] }],
authority(),
);
expect(state.trustSeeds.has(CREATOR)).toBe(true);
expect(state.contributors.size).toBe(0);
});
it("records address denials by coordinate", () => {
const state = reduceAdminState(
[{ actor: MODERATOR, kind: "address-deny", targets: [address("30023", CREATOR, "listing")] }],
authority(),
);
expect(state.addressDeny.has(`address:30023:${CREATOR}:listing`)).toBe(true);
});
it("returns empty state for non-array input", () => {
expect(reduceAdminState(/** @type {any} */ (null), authority()).userDeny.size).toBe(0);
});
});
describe("protected actors", () => {
it("cannot be denied through a contributor list", () => {
const state = reduceAdminState(
[{ actor: MODERATOR, kind: "user-deny", targets: [user(ROOT)] }],
authority(),
);
expect(state.userDeny.has(`user:${ROOT}`)).toBe(false);
});
it("cannot be denied even by many contributors", () => {
const state = reduceAdminState(
[
{ actor: MODERATOR, kind: "user-deny", targets: [user(ROOT)] },
{ actor: CURATOR, kind: "user-deny", targets: [user(ROOT)] },
],
authority(),
);
expect(state.userDeny.has(`user:${ROOT}`)).toBe(false);
expect(state.contributors.has(`user:${ROOT}`)).toBe(false);
});
});
describe("community sources", () => {
it("marks only contributions carrying a source", () => {
const state = reduceAdminState(
[
{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] },
{ actor: CURATOR, kind: "user-deny", source: "list-a", targets: [user(STRANGER)] },
],
authority(),
);
expect(state.communitySources.has(`user:${CREATOR}`)).toBe(false);
expect(state.communitySources.get(`user:${STRANGER}`)).toEqual(["list-a"]);
});
it("merges a community list into existing state", () => {
const state = reduceAdminState(
[{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] }],
authority(),
);
mergeCommunitySource(
state,
{ actor: CURATOR, kind: "user-deny", source: "list-b", targets: [user(STRANGER)] },
authority(),
);
expect(state.userDeny.has(`user:${CREATOR}`)).toBe(true);
expect(state.userDeny.has(`user:${STRANGER}`)).toBe(true);
expect(state.communitySources.get(`user:${STRANGER}`)).toEqual(["list-b"]);
});
it("still protects the root when merging a community list", () => {
const state = createEmptyAdminState();
mergeCommunitySource(
state,
{ actor: CURATOR, kind: "user-deny", source: "hostile", targets: [user(ROOT)] },
authority(),
);
expect(state.userDeny.has(`user:${ROOT}`)).toBe(false);
});
it("ignores a community list from an unauthorized curator", () => {
const state = createEmptyAdminState();
mergeCommunitySource(
state,
{ actor: STRANGER, kind: "user-deny", source: "list-c", targets: [user(CREATOR)] },
authority(),
);
expect(state.userDeny.size).toBe(0);
});
});
describe("isDenied", () => {
const state = reduceAdminState(
[
{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] },
{ actor: MODERATOR, kind: "event-deny", targets: [event(EVENT_ID)] },
{ actor: MODERATOR, kind: "address-deny", targets: [address("30023", CREATOR, "listing")] },
],
authority(),
);
it("detects denial per target type", () => {
expect(isDenied(user(CREATOR), state)).toBe(true);
expect(isDenied(event(EVENT_ID), state)).toBe(true);
expect(isDenied(address("30023", CREATOR, "listing"), state)).toBe(true);
});
it("does not leak denial across target types", () => {
expect(isDenied(user(STRANGER), state)).toBe(false);
expect(isDenied(address("30023", CREATOR, "other"), state)).toBe(false);
});
});
describe("serializeAdminState", () => {
it("produces sorted, stable output", () => {
const state = reduceAdminState(
[{ actor: MODERATOR, kind: "user-deny", targets: [user(STRANGER), user(CREATOR)] }],
authority(),
);
const serialized = serializeAdminState(state);
expect(serialized.userDeny).toEqual([...serialized.userDeny].sort());
expect(serializeAdminState(state)).toEqual(serialized);
});
});
+121 -326
View File
@@ -1,392 +1,187 @@
import { describe, it, expect } from "vitest";
import { describe, expect, it } from "vitest";
import {
BUILTIN_ROLES,
getRoleCapabilities,
hasRole,
DEFAULT_ROLE_CAPABILITIES,
GOVERNANCE_CAPABILITIES,
createAuthorityState,
createRoleDefinition,
getActorCapabilities,
getActorRoles,
hasCapability,
getActorCapabilities,
createRoleDefinition,
createActor,
createAuthorityState
hasRole,
isGovernanceCapability,
isProtectedActor,
} from "../src/authority.js";
describe("authority", () => {
describe("BUILTIN_ROLES", () => {
it("should define built-in roles with capabilities", () => {
expect(BUILTIN_ROLES).toHaveProperty("root");
expect(BUILTIN_ROLES).toHaveProperty("administrator");
expect(BUILTIN_ROLES).toHaveProperty("moderator");
expect(BUILTIN_ROLES).toHaveProperty("trusted-user");
expect(BUILTIN_ROLES).toHaveProperty("user");
const ROOT = "a1".repeat(32);
const MODERATOR = "b2".repeat(32);
const CURATOR = "c3".repeat(32);
const STRANGER = "d4".repeat(32);
// Check that each role has a name and capabilities
for (const [name, role] of Object.entries(BUILTIN_ROLES)) {
expect(role.name).toBe(name);
expect(Array.isArray(role.capabilities)).toBe(true);
expect(role.capabilities.length).toBeGreaterThan(0);
}
});
const baseState = () =>
createAuthorityState({
root: ROOT,
actors: {
[ROOT]: ["super_admin"],
[MODERATOR]: ["moderator"],
[CURATOR]: ["curator"],
},
});
describe("getRoleCapabilities", () => {
it("should return capabilities for built-in roles", () => {
const capabilities = getRoleCapabilities("root");
expect(capabilities).toEqual(BUILTIN_ROLES.root.capabilities);
});
it("should return capabilities for custom roles", () => {
const authorityState = createAuthorityState({
"custom-role": {
name: "custom-role",
capabilities: ["custom-capability"]
describe("capabilities", () => {
it("recognizes every documented capability", () => {
for (const capability of GOVERNANCE_CAPABILITIES) {
expect(isGovernanceCapability(capability)).toBe(true);
}
});
const capabilities = getRoleCapabilities("custom-role", authorityState);
expect(capabilities).toEqual(["custom-capability"]);
it("rejects unknown capabilities", () => {
expect(isGovernanceCapability("delete-everything")).toBe(false);
expect(isGovernanceCapability("")).toBe(false);
expect(isGovernanceCapability(null)).toBe(false);
});
it("should prefer custom roles over built-in roles", () => {
const authorityState = createAuthorityState({
"root": {
name: "root",
capabilities: ["custom-root-capability"]
}
});
const capabilities = getRoleCapabilities("root", authorityState);
expect(capabilities).toEqual(["custom-root-capability"]);
});
it("should return empty array for unknown roles", () => {
const capabilities = getRoleCapabilities("unknown-role");
expect(capabilities).toEqual([]);
});
});
describe("hasRole", () => {
it("should return true for actors with the specified role", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["moderator"]
}
});
const result = hasRole(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"moderator",
authorityState
it("gives super_admin every capability", () => {
expect([...DEFAULT_ROLE_CAPABILITIES.super_admin].sort()).toEqual(
[...GOVERNANCE_CAPABILITIES].sort(),
);
expect(result).toBe(true);
});
it("should return false for actors without the specified role", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["user"]
it("keeps curators strictly weaker than moderators", () => {
const curator = new Set(DEFAULT_ROLE_CAPABILITIES.curator);
const moderator = new Set(DEFAULT_ROLE_CAPABILITIES.moderator);
for (const capability of curator) {
expect(moderator.has(capability)).toBe(true);
}
expect(moderator.size).toBeGreaterThan(curator.size);
});
const result = hasRole(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"moderator",
authorityState
);
expect(result).toBe(false);
it("does not let a curator deny events or addresses", () => {
expect(DEFAULT_ROLE_CAPABILITIES.curator).not.toContain("contribute-event-deny");
expect(DEFAULT_ROLE_CAPABILITIES.curator).not.toContain("contribute-address-deny");
});
it("should return false for unknown actors", () => {
const authorityState = createAuthorityState();
const result = hasRole(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"moderator",
authorityState
);
expect(result).toBe(false);
it("does not let a moderator manage roles or policy", () => {
expect(DEFAULT_ROLE_CAPABILITIES.moderator).not.toContain("manage-roles");
expect(DEFAULT_ROLE_CAPABILITIES.moderator).not.toContain("manage-policy");
});
});
it("should normalize pubkeys to lowercase", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["moderator"]
}
describe("createAuthorityState", () => {
it("normalizes actor pubkeys and drops invalid ones", () => {
const state = createAuthorityState({
actors: { [MODERATOR.toUpperCase()]: ["moderator"], "not-a-key": ["moderator"] },
});
expect(state.actors[MODERATOR]).toEqual(["moderator"]);
expect(Object.keys(state.actors)).toHaveLength(1);
});
const result = hasRole(
"ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789",
"moderator",
authorityState
);
expect(result).toBe(true);
});
it("drops actors with no roles", () => {
const state = createAuthorityState({ actors: { [MODERATOR]: [] } });
expect(state.actors[MODERATOR]).toBeUndefined();
});
describe("getActorRoles", () => {
it("should return roles for known actors", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["user", "trusted-user"]
}
it("deduplicates repeated roles", () => {
const state = createAuthorityState({ actors: { [MODERATOR]: ["moderator", "moderator"] } });
expect(state.actors[MODERATOR]).toEqual(["moderator"]);
});
const roles = getActorRoles(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
authorityState
);
expect(roles).toEqual(["user", "trusted-user"]);
it("always protects the root administrator", () => {
const state = createAuthorityState({ root: ROOT });
expect(state.protectedActors).toContain(ROOT);
});
it("should return empty array for unknown actors", () => {
const authorityState = createAuthorityState();
const roles = getActorRoles(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
authorityState
);
expect(roles).toEqual([]);
});
it("keeps explicitly protected actors alongside root", () => {
const state = createAuthorityState({ root: ROOT, protectedActors: [MODERATOR] });
expect(state.protectedActors).toContain(ROOT);
expect(state.protectedActors).toContain(MODERATOR);
});
describe("hasCapability", () => {
it("should return true for actors with the specified capability", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["moderator"]
}
it("filters unknown capabilities out of custom roles", () => {
const state = createAuthorityState({
roles: { odd: /** @type {any} */ (["manage-policy", "not-a-capability"]) },
});
const result = hasCapability(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"submit-reports",
authorityState
);
expect(result).toBe(true);
expect(state.roles.odd).toEqual(["manage-policy"]);
});
});
it("should return false for actors without the specified capability", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["user"]
}
describe("capability resolution", () => {
it("resolves capabilities through roles", () => {
const state = baseState();
expect(hasCapability(MODERATOR, "contribute-event-deny", state)).toBe(true);
expect(hasCapability(CURATOR, "contribute-user-deny", state)).toBe(true);
});
const result = hasCapability(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"manage-admin-lists",
authorityState
);
expect(result).toBe(false);
it("denies capabilities a role does not carry", () => {
const state = baseState();
expect(hasCapability(CURATOR, "contribute-event-deny", state)).toBe(false);
expect(hasCapability(MODERATOR, "manage-roles", state)).toBe(false);
});
it("should return false for unknown actors", () => {
const authorityState = createAuthorityState();
const result = hasCapability(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
"submit-reports",
authorityState
);
expect(result).toBe(false);
});
it("gives the root every capability even without a role entry", () => {
const state = createAuthorityState({ root: ROOT });
expect(hasCapability(ROOT, "manage-roles", state)).toBe(true);
expect(getActorCapabilities(ROOT, state).sort()).toEqual([...GOVERNANCE_CAPABILITIES].sort());
});
describe("getActorCapabilities", () => {
it("should return all capabilities for an actor's roles", () => {
const authorityState = createAuthorityState({}, {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["moderator", "trusted-user"]
}
it("returns nothing for unknown actors", () => {
const state = baseState();
expect(getActorCapabilities(STRANGER, state)).toEqual([]);
expect(getActorRoles(STRANGER, state)).toEqual([]);
expect(hasCapability(STRANGER, "contribute-user-deny", state)).toBe(false);
});
const capabilities = getActorCapabilities(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
authorityState
);
it("stops granting capabilities the moment a role is revoked", () => {
const before = baseState();
expect(hasCapability(MODERATOR, "contribute-user-deny", before)).toBe(true);
// Should have capabilities from both roles, with duplicates removed
expect(capabilities).toContain("submit-reports");
expect(capabilities).toContain("view-reports");
expect(capabilities).toContain("submit-trust-endorsements");
const after = createAuthorityState({ root: ROOT, actors: { [ROOT]: ["super_admin"] } });
expect(hasCapability(MODERATOR, "contribute-user-deny", after)).toBe(false);
});
it("should return empty array for unknown actors", () => {
const authorityState = createAuthorityState();
const capabilities = getActorCapabilities(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
authorityState
);
expect(capabilities).toEqual([]);
});
it("normalizes pubkey case when resolving", () => {
const state = baseState();
expect(hasRole(MODERATOR.toUpperCase(), "moderator", state)).toBe(true);
});
describe("createRoleDefinition", () => {
it("should create a valid role definition", () => {
const role = createRoleDefinition("test-role", ["capability1", "capability2"]);
expect(role).toEqual({
name: "test-role",
capabilities: ["capability1", "capability2"]
});
it("rejects unknown capability names outright", () => {
const state = baseState();
expect(hasCapability(ROOT, /** @type {any} */ ("nope"), state)).toBe(false);
});
});
it("should trim whitespace from role name and capabilities", () => {
const role = createRoleDefinition(" test-role ", [" capability1 ", " capability2 "]);
expect(role).toEqual({
name: "test-role",
capabilities: ["capability1", "capability2"]
describe("protected actors", () => {
it("reports protection for the root", () => {
expect(isProtectedActor(ROOT, baseState())).toBe(true);
});
});
it("should throw error for invalid role name", () => {
expect(() => createRoleDefinition("", ["capability"])).toThrow();
expect(() => createRoleDefinition(/** @type {any} */ (123), ["capability"])).toThrow();
it("does not protect ordinary actors", () => {
expect(isProtectedActor(MODERATOR, baseState())).toBe(false);
});
it("should throw error for invalid capabilities", () => {
expect(() => createRoleDefinition("test-role", /** @type {any} */ ("not-an-array"))).toThrow();
expect(() => createRoleDefinition("test-role", [/** @type {any} */ (123)])).toThrow();
expect(() => createRoleDefinition("test-role", [""])).toThrow();
});
it("handles invalid input", () => {
expect(isProtectedActor("", baseState())).toBe(false);
});
});
describe("createActor", () => {
it("should create a valid actor", () => {
const actor = createActor(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
["role1", "role2"]
);
expect(actor).toEqual({
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["role1", "role2"]
describe("createRoleDefinition", () => {
it("creates a role from valid capabilities", () => {
const role = createRoleDefinition("listing_moderator", ["contribute-address-deny"]);
expect(role).toEqual({ name: "listing_moderator", capabilities: ["contribute-address-deny"] });
});
});
it("should normalize pubkey to lowercase", () => {
const actor = createActor(
"ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789ABCDEF0123456789",
["role1"]
);
expect(actor.pubkey).toBe("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
it("deduplicates capabilities", () => {
const role = createRoleDefinition("r", ["review-evidence", "review-evidence"]);
expect(role.capabilities).toEqual(["review-evidence"]);
});
it("should trim whitespace from roles", () => {
const actor = createActor(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
[" role1 ", " role2 "]
it("rejects an unknown capability rather than silently dropping it", () => {
expect(() => createRoleDefinition("r", [/** @type {any} */ ("invent-capability")])).toThrow(
/Unknown governance capability/,
);
expect(actor.roles).toEqual(["role1", "role2"]);
});
it("should throw error for invalid pubkey", () => {
expect(() => createActor("invalid-pubkey", ["role"])).toThrow();
expect(() => createActor(/** @type {any} */ (123), ["role"])).toThrow();
expect(() => createActor("", ["role"])).toThrow();
});
it("should throw error for invalid roles", () => {
expect(() => createActor(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
/** @type {any} */ ("not-an-array")
)).toThrow();
expect(() => createActor(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
[/** @type {any} */ (123)]
)).toThrow();
expect(() => createActor(
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
[""]
)).toThrow();
});
});
describe("createAuthorityState", () => {
it("should create a valid authority state", () => {
const roles = {
"test-role": {
name: "test-role",
capabilities: ["test-capability"]
}
};
const actors = {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789",
roles: ["test-role"]
}
};
const state = createAuthorityState(roles, actors);
expect(state).toEqual({ roles, actors });
});
it("should create empty authority state with no arguments", () => {
const state = createAuthorityState();
expect(state).toEqual({ roles: {}, actors: {} });
});
it("should validate role names", () => {
const roles = {
"": {
name: "",
capabilities: ["test-capability"]
}
};
expect(() => createAuthorityState(roles)).toThrow();
});
it("should validate role definitions", () => {
const roles = {
"test-role": /** @type {any} */ (null)
};
expect(() => createAuthorityState(roles)).toThrow();
});
it("should validate role name consistency", () => {
const roles = {
"test-role": {
name: "different-name",
capabilities: ["test-capability"]
}
};
expect(() => createAuthorityState(roles)).toThrow();
});
it("should validate actor pubkeys", () => {
const actors = {
"invalid-pubkey": {
pubkey: "invalid-pubkey",
roles: ["test-role"]
}
};
expect(() => createAuthorityState({}, actors)).toThrow();
});
it("should validate actors", () => {
const actors = {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": /** @type {any} */ (null)
};
expect(() => createAuthorityState({}, actors)).toThrow();
});
it("should validate actor pubkey consistency", () => {
const actors = {
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": {
pubkey: "different-pubkey",
roles: ["test-role"]
}
};
expect(() => createAuthorityState({}, actors)).toThrow();
});
it("rejects an empty name", () => {
expect(() => createRoleDefinition(" ", [])).toThrow(/non-empty/);
});
});
+503 -291
View File
@@ -1,369 +1,581 @@
import { describe, it, expect } from "vitest";
import { describe, expect, it } from "vitest";
import { createAuthorityState } from "../src/authority.js";
import { reduceAdminState } from "../src/adminState.js";
import { createPolicyDefinition } from "../src/policy.js";
import {
evaluateTarget,
aggregateMutes,
aggregateReports,
createSnapshot,
createViewerState,
evaluateMany,
createGovernanceState,
createReport,
createTrustedMute,
createOverride,
DEFAULT_THRESHOLDS
evaluateTarget,
resolveTrustSet,
} from "../src/evaluator.js";
import {
createUserTarget,
createEventTarget
} from "../src/identifiers.js";
import {
createAuthorityState,
createActor
} from "../src/authority.js";
import {
createPolicyContext
} from "../src/policy.js";
describe("evaluator", () => {
describe("evaluateTarget", () => {
it("should return default allow decision for target with no reports or mutes", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const state = createGovernanceState();
const context = createPolicyContext("feed", "default");
const ROOT = "a1".repeat(32);
const MODERATOR = "b2".repeat(32);
const CREATOR = "d4".repeat(32);
const STRANGER = "f6".repeat(32);
const EVENT_ID = "1b".repeat(32);
const NOW = 1_750_000_000;
const DAY = 86_400;
const decision = evaluateTarget(target, state, context);
expect(decision).toEqual({
visibility: { effect: "allow" },
interaction: { effect: "allow" },
reasons: [],
evidence: []
});
const trusted = (index) => index.toString(16).padStart(2, "0").repeat(32);
const TRUSTED = Array.from({ length: 24 }, (_, index) => trusted(index + 1));
/** @param {string} pubkey @returns {import("../src/identifiers.js").UserTarget} */
const user = (pubkey) => ({ type: "user", pubkey });
/** @param {string} id @param {string} author @returns {import("../src/identifiers.js").EventTarget} */
const event = (id, author) => ({ type: "event", id, author });
const POLICY = createPolicyDefinition({
id: "test",
version: "2.1.0",
defaultProfile: "feed",
profiles: {
feed: {
name: "feed",
administrativeDeny: { visibility: "hide", interaction: "deny" },
viewerBlock: { visibility: "hide", interaction: "deny" },
allowViewerOverride: true,
muteWindowSeconds: 60 * DAY,
reports: { default: { restrict: 3 }, spam: { hide: 5 } },
mutes: { default: { downrank: 1, hide: 20 } },
},
strict: {
name: "strict",
administrativeDeny: { visibility: "deny", interaction: "deny" },
allowViewerOverride: false,
requireAllowlist: true,
allowlistMiss: { visibility: "hide", interaction: "deny" },
exposeEvidence: true,
reports: {},
mutes: {},
},
off: { name: "off", disabled: true },
commerce: {
name: "commerce",
administrativeDeny: { visibility: "allow", transaction: "deny" },
reports: { scam: { transactionReview: 2, transactionDeny: 4 } },
mutes: {},
},
},
});
const authority = () =>
createAuthorityState({
root: ROOT,
actors: { [ROOT]: ["super_admin"], [MODERATOR]: ["moderator"] },
});
it("should hide target with sufficient trusted reports", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const reporter = "1234567890123456789012345678901234567890123456789012345678901234";
/**
* @param {Object} [parts]
* @param {import("../src/adminState.js").Contribution[]} [parts.contributions]
* @param {{ contacts?: Set<string>, seeds?: Set<string> }} [parts.trust]
* @param {Map<string, import("../src/evaluator.js").ReportRecord[]>} [parts.reports]
* @param {Map<string, import("../src/evaluator.js").MuteRecord[]>} [parts.trustedMutes]
*/
function snapshotWith({ contributions = [], trust, reports, trustedMutes } = {}) {
const auth = authority();
return createSnapshot({
authority: auth,
admin: reduceAdminState(contributions, auth),
trust: trust ?? { contacts: new Set(TRUSTED) },
reports: reports ?? new Map(),
trustedMutes: trustedMutes ?? new Map(),
});
}
// Create authority state with reporter as moderator (can submit reports)
const authorityState = createAuthorityState({}, {
[reporter]: createActor(reporter, ["moderator"])
const context = (profile = "feed") => ({
surface: profile,
policyProfile: profile,
policy: POLICY,
now: NOW,
});
const reportsFrom = (count, category) =>
TRUSTED.slice(0, count).map((reporter) => ({ reporter, category, createdAt: NOW - DAY }));
const mutesFrom = (count, updatedAt = NOW - DAY) =>
TRUSTED.slice(0, count).map((muter) => ({ muter, updatedAt }));
describe("evaluateTarget", () => {
it("allows a target with no signals", () => {
const decision = evaluateTarget(user(CREATOR), snapshotWith(), context());
expect(decision.visibility.effect).toBe("allow");
expect(decision.interaction.effect).toBe("allow");
expect(decision.ranking.effect).toBe("normal");
expect(decision.reasons).toEqual([]);
});
const state = createGovernanceState(
authorityState,
{
"user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": [
createReport(reporter, target, "spam", Date.now())
]
}
it("rejects an invalid target", () => {
expect(() => evaluateTarget(/** @type {any} */ ({ type: "user", pubkey: "nope" }), snapshotWith(), context())).toThrow(
/Invalid governance target/,
);
});
const context = createPolicyContext("feed", "default");
it("stamps policy identity and a snapshot fingerprint on the decision", () => {
const decision = evaluateTarget(user(CREATOR), snapshotWith(), context());
expect(decision.policyProfile).toBe("feed");
expect(decision.policyVersion).toBe("2.1.0");
expect(decision.evaluatedAt).toBe(NOW);
expect(decision.snapshotFingerprint).toMatch(/^[0-9a-f]{16}$/);
expect(decision.key).toBe(`user:${CREATOR}`);
});
// Add enough reports to exceed the hide threshold
for (let i = 0; i < DEFAULT_THRESHOLDS.trustedReportHideThreshold; i++) {
state.reports["user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"].push(
createReport(reporter, target, "spam", Date.now() + i)
);
}
it("reports a disabled surface instead of pretending to evaluate", () => {
const snapshot = snapshotWith({
contributions: [{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] }],
});
const decision = evaluateTarget(user(CREATOR), snapshot, context("off"));
expect(decision.visibility.effect).toBe("allow");
expect(decision.reasons.map((r) => r.id)).toEqual(["policy-disabled"]);
});
});
const decision = evaluateTarget(target, state, context);
describe("administrative denial", () => {
it("applies the profile's denial effects", () => {
const snapshot = snapshotWith({
contributions: [{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] }],
});
const decision = evaluateTarget(user(CREATOR), snapshot, context());
expect(decision.visibility.effect).toBe("hide");
expect(decision.interaction.effect).toBe("deny");
expect(decision.reasons.map((r) => r.id)).toContain("admin-user-deny");
});
it("should restrict target with moderate number of trusted reports", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const reporter = "1234567890123456789012345678901234567890123456789012345678901234";
// Create authority state with reporter as moderator (can submit reports)
const authorityState = createAuthorityState({}, {
[reporter]: createActor(reporter, ["moderator"])
it("varies by surface", () => {
const snapshot = snapshotWith({
contributions: [{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] }],
});
expect(evaluateTarget(user(CREATOR), snapshot, context("strict")).visibility.effect).toBe("deny");
});
const state = createGovernanceState(
authorityState,
{
"user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789": [
createReport(reporter, target, "spam", Date.now())
]
}
);
const context = createPolicyContext("feed", "default");
// Add enough reports to exceed the restrict threshold but not the hide threshold
for (let i = 0; i < DEFAULT_THRESHOLDS.trustedReportRestrictThreshold; i++) {
state.reports["user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"].push(
createReport(reporter, target, "spam", Date.now() + i)
);
}
const decision = evaluateTarget(target, state, context);
expect(decision.visibility.effect).toBe("restrict");
expect(decision.interaction.effect).toBe("allow");
it("reaches an author's events", () => {
const snapshot = snapshotWith({
contributions: [{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] }],
});
expect(evaluateTarget(event(EVENT_ID, CREATOR), snapshot, context()).visibility.effect).toBe("hide");
});
it("should hide target with sufficient trusted mutes", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
it("does not reach events of other authors", () => {
const snapshot = snapshotWith({
contributions: [{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] }],
});
expect(evaluateTarget(event(EVENT_ID, STRANGER), snapshot, context()).visibility.effect).toBe("allow");
});
const state = createGovernanceState(
undefined,
undefined,
{
"user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789":
createTrustedMute(target, DEFAULT_THRESHOLDS.trustedMuteHideThreshold, {
"mute": DEFAULT_THRESHOLDS.trustedMuteHideThreshold
})
}
);
it("leaves a protected actor untouched", () => {
const snapshot = snapshotWith({
contributions: [{ actor: MODERATOR, kind: "user-deny", targets: [user(ROOT)] }],
});
const decision = evaluateTarget(user(ROOT), snapshot, context());
expect(decision.visibility.effect).toBe("allow");
expect(decision.evidence?.protectedTarget).toBe(true);
});
const context = createPolicyContext("feed", "default");
it("blocks a transaction while leaving a product visible", () => {
const snapshot = snapshotWith({
contributions: [{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] }],
});
const decision = evaluateTarget(user(CREATOR), snapshot, context("commerce"));
expect(decision.visibility.effect).toBe("allow");
expect(decision.transaction?.effect).toBe("deny");
});
});
const decision = evaluateTarget(target, state, context);
describe("allowlist", () => {
it("hides a target that misses the allowlist when the profile requires one", () => {
const decision = evaluateTarget(user(CREATOR), snapshotWith(), context("strict"));
expect(decision.visibility.effect).toBe("hide");
expect(decision.interaction.effect).toBe("deny");
expect(decision.reasons.map((r) => r.id)).toContain("allowlist-miss");
});
it("should restrict target with moderate number of trusted mutes", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const state = createGovernanceState(
undefined,
undefined,
{
"user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789":
createTrustedMute(target, DEFAULT_THRESHOLDS.trustedMuteRestrictThreshold, {
"mute": DEFAULT_THRESHOLDS.trustedMuteRestrictThreshold
})
}
);
const context = createPolicyContext("feed", "default");
const decision = evaluateTarget(target, state, context);
expect(decision.visibility.effect).toBe("restrict");
expect(decision.interaction.effect).toBe("allow");
it("permits an allowlisted target", () => {
const snapshot = snapshotWith({
contributions: [{ actor: ROOT, kind: "user-allow", targets: [user(CREATOR)] }],
});
const decision = evaluateTarget(user(CREATOR), snapshot, context("strict"));
expect(decision.visibility.effect).toBe("allow");
expect(decision.evidence?.userAllowed).toBe(true);
});
it("should apply explicit overrides", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
it("does not enforce the allowlist on surfaces that do not require it", () => {
expect(evaluateTarget(user(CREATOR), snapshotWith(), context()).visibility.effect).toBe("allow");
});
const state = createGovernanceState(
undefined,
undefined,
undefined,
{
"user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789":
createOverride(target, "hide", "Administrative override")
}
);
it("grants no trust from being allowlisted", () => {
const snapshot = snapshotWith({
contributions: [{ actor: ROOT, kind: "user-allow", targets: [user(CREATOR)] }],
trust: { contacts: new Set() },
reports: new Map([[`user:${STRANGER}`, [{ reporter: CREATOR, category: "spam", createdAt: NOW }]]]),
});
const decision = evaluateTarget(user(STRANGER), snapshot, context());
expect(decision.evidence?.trustedReportTotal).toBe(0);
});
});
const context = createPolicyContext("feed", "default");
const decision = evaluateTarget(target, state, context);
describe("viewer state", () => {
it("hides a blocked author", () => {
const viewer = createViewerState({ blocks: new Set([CREATOR]) });
const decision = evaluateTarget(user(CREATOR), snapshotWith(), context(), viewer);
expect(decision.visibility.effect).toBe("hide");
expect(decision.interaction.effect).toBe("hide");
expect(decision.reasons).toContain("Administrative override");
expect(decision.evidence?.personalBlock).toBe(true);
});
it("should ignore overrides when enforcement disallows them", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const state = createGovernanceState(
undefined,
undefined,
undefined,
{
"user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789":
createOverride(target, "hide", "Administrative override")
}
it("applies a block through to the author's events", () => {
const viewer = createViewerState({ blocks: new Set([CREATOR]) });
expect(evaluateTarget(event(EVENT_ID, CREATOR), snapshotWith(), context(), viewer).visibility.effect).toBe(
"hide",
);
const context = createPolicyContext("feed", "default", {
allowOverrides: false
});
const decision = evaluateTarget(target, state, context);
it("records a viewer mute without changing effects", () => {
const viewer = createViewerState({ mutes: new Set([CREATOR]) });
const decision = evaluateTarget(user(CREATOR), snapshotWith(), context(), viewer);
expect(decision.visibility.effect).toBe("allow");
expect(decision.evidence?.personalMute).toBe(true);
expect(decision.reasons.map((r) => r.id)).toContain("viewer-mute");
});
it("softens a decision through an override and requires explicit action", () => {
const viewer = createViewerState({
overrides: new Map([[`user:${CREATOR}`, { visibility: "allow" }]]),
});
const snapshot = snapshotWith({ trustedMutes: new Map([[`user:${CREATOR}`, mutesFrom(20)]]) });
const decision = evaluateTarget(user(CREATOR), snapshot, context(), viewer);
expect(decision.visibility.effect).toBe("allow");
expect(decision.interaction.effect).toBe("require-explicit-action");
});
it("never lets an override escalate", () => {
const viewer = createViewerState({
overrides: new Map([[`user:${CREATOR}`, { visibility: "deny" }]]),
});
const decision = evaluateTarget(user(CREATOR), snapshotWith(), context(), viewer);
expect(decision.visibility.effect).toBe("allow");
expect(decision.reasons.map((r) => r.id)).not.toContain("viewer-override");
});
it("ignores overrides on a profile that forbids them", () => {
const viewer = createViewerState({
overrides: new Map([[`user:${CREATOR}`, { visibility: "allow" }]]),
});
expect(evaluateTarget(user(CREATOR), snapshotWith(), context("strict"), viewer).visibility.effect).toBe(
"hide",
);
});
it("ignores overrides when enforcement disables them", () => {
const viewer = createViewerState({
overrides: new Map([[`user:${CREATOR}`, { visibility: "allow" }]]),
});
const snapshot = snapshotWith({ trustedMutes: new Map([[`user:${CREATOR}`, mutesFrom(20)]]) });
const decision = evaluateTarget(
user(CREATOR),
snapshot,
{ ...context(), enforcement: { allowOverrides: false } },
viewer,
);
expect(decision.visibility.effect).toBe("hide");
});
it("ignores an override naming an unknown effect", () => {
const viewer = createViewerState({
overrides: new Map([[`user:${CREATOR}`, /** @type {any} */ ({ visibility: "sideways" })]]),
});
const snapshot = snapshotWith({ trustedMutes: new Map([[`user:${CREATOR}`, mutesFrom(20)]]) });
expect(evaluateTarget(user(CREATOR), snapshot, context(), viewer).visibility.effect).toBe("hide");
});
});
describe("trust resolution", () => {
it("prefers real contacts over seeds", () => {
const set = resolveTrustSet({ contacts: new Set([CREATOR]), seeds: new Set([STRANGER]) });
expect(set.has(CREATOR)).toBe(true);
expect(set.has(STRANGER)).toBe(false);
});
it("falls back to seeds when the viewer follows nobody", () => {
const set = resolveTrustSet({ contacts: new Set(), seeds: new Set([STRANGER]) });
expect(set.has(STRANGER)).toBe(true);
});
it("returns an empty set when neither is present", () => {
expect(resolveTrustSet(undefined).size).toBe(0);
});
});
describe("report aggregation", () => {
it("restricts at the threshold", () => {
const snapshot = snapshotWith({ reports: new Map([[`user:${CREATOR}`, reportsFrom(3, "nudity")]]) });
expect(evaluateTarget(user(CREATOR), snapshot, context()).visibility.effect).toBe("restrict");
});
it("stays permissive below the threshold", () => {
const snapshot = snapshotWith({ reports: new Map([[`user:${CREATOR}`, reportsFrom(2, "nudity")]]) });
expect(evaluateTarget(user(CREATOR), snapshot, context()).visibility.effect).toBe("allow");
});
it("deduplicates a reporter within a category", () => {
const records = [
{ reporter: TRUSTED[0], category: "spam", createdAt: NOW },
{ reporter: TRUSTED[0], category: "spam", createdAt: NOW - 1 },
];
const result = aggregateReports(
"k",
{ ...snapshotWith(), reports: new Map([["k", records]]) },
{},
new Set(TRUSTED),
);
expect(result.total).toBe(1);
});
it("counts distinct categories from the same reporter separately", () => {
const records = [
{ reporter: TRUSTED[0], category: "spam", createdAt: NOW },
{ reporter: TRUSTED[0], category: "nudity", createdAt: NOW },
];
const result = aggregateReports(
"k",
{ ...snapshotWith(), reports: new Map([["k", records]]) },
{},
new Set(TRUSTED),
);
expect(result.total).toBe(2);
expect(result.byCategory).toEqual({ spam: 1, nudity: 1 });
});
it("skips untrusted, blocked, and denied reporters", () => {
const snapshot = snapshotWith({
contributions: [{ actor: MODERATOR, kind: "user-deny", targets: [user(TRUSTED[1])] }],
reports: new Map([
[
`user:${CREATOR}`,
[
{ reporter: TRUSTED[0], category: "spam", createdAt: NOW },
{ reporter: TRUSTED[1], category: "spam", createdAt: NOW },
{ reporter: TRUSTED[2], category: "spam", createdAt: NOW },
{ reporter: STRANGER, category: "spam", createdAt: NOW },
],
],
]),
});
const viewer = createViewerState({ blocks: new Set([TRUSTED[2]]) });
const decision = evaluateTarget(user(CREATOR), snapshot, context(), viewer);
expect(decision.evidence?.trustedReportTotal).toBe(1);
});
it("ignores reports with no category", () => {
const result = aggregateReports(
"k",
{ ...snapshotWith(), reports: new Map([["k", [{ reporter: TRUSTED[0], category: " " }]]]) },
{},
new Set(TRUSTED),
);
expect(result.total).toBe(0);
});
it("applies category-specific thresholds", () => {
const snapshot = snapshotWith({ reports: new Map([[`user:${CREATOR}`, reportsFrom(5, "spam")]]) });
expect(evaluateTarget(user(CREATOR), snapshot, context()).visibility.effect).toBe("hide");
});
it("drives the transaction dimension on a commerce surface", () => {
const snapshot = snapshotWith({ reports: new Map([[`user:${CREATOR}`, reportsFrom(2, "scam")]]) });
expect(evaluateTarget(user(CREATOR), snapshot, context("commerce")).transaction?.effect).toBe(
"require-review",
);
});
});
describe("mute aggregation", () => {
it("downranks below the hide threshold without hiding", () => {
const snapshot = snapshotWith({ trustedMutes: new Map([[`user:${CREATOR}`, mutesFrom(19)]]) });
const decision = evaluateTarget(user(CREATOR), snapshot, context());
expect(decision.ranking.effect).toBe("downrank");
expect(decision.visibility.effect).toBe("allow");
expect(decision.interaction.effect).toBe("allow");
});
it("hides at the threshold", () => {
const snapshot = snapshotWith({ trustedMutes: new Map([[`user:${CREATOR}`, mutesFrom(20)]]) });
expect(evaluateTarget(user(CREATOR), snapshot, context()).visibility.effect).toBe("hide");
});
describe("evaluateMany", () => {
it("should evaluate multiple targets", () => {
const target1 = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const target2 = createEventTarget("1234567890123456789012345678901234567890123456789012345678901234");
const state = createGovernanceState();
const context = createPolicyContext("feed", "default");
const results = evaluateMany([target1, target2], state, context);
expect(results).toHaveProperty("user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(results).toHaveProperty("event:1234567890123456789012345678901234567890123456789012345678901234");
expect(results["user:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"]).toEqual({
visibility: { effect: "allow" },
interaction: { effect: "allow" },
reasons: [],
evidence: []
});
expect(results["event:1234567890123456789012345678901234567890123456789012345678901234"]).toEqual({
visibility: { effect: "allow" },
interaction: { effect: "allow" },
reasons: [],
evidence: []
});
});
});
describe("createGovernanceState", () => {
it("should create a valid governance state", () => {
const authority = createAuthorityState();
const reports = {};
const trustedMutes = {};
const overrides = {};
const policies = {};
const state = createGovernanceState(authority, reports, trustedMutes, overrides, policies);
expect(state).toEqual({
authority,
reports,
trustedMutes,
overrides,
policies
});
});
it("should create a governance state with default empty objects", () => {
const state = createGovernanceState();
expect(state).toEqual({
authority: { roles: {}, actors: {} },
reports: {},
trustedMutes: {},
overrides: {},
policies: {}
});
});
});
describe("createReport", () => {
it("should create a valid report", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const report = createReport(
"1234567890123456789012345678901234567890123456789012345678901234",
target,
"spam",
Date.now()
it("counts muters uniquely", () => {
const records = [
{ muter: TRUSTED[0], updatedAt: NOW },
{ muter: TRUSTED[0], updatedAt: NOW - 1 },
{ muter: TRUSTED[1], updatedAt: NOW },
];
const result = aggregateMutes(
"k",
{ ...snapshotWith(), trustedMutes: new Map([["k", records]]) },
{},
new Set(TRUSTED),
NOW,
60 * DAY,
);
expect(report).toEqual({
reporter: "1234567890123456789012345678901234567890123456789012345678901234",
target,
category: "spam",
timestamp: expect.any(Number)
});
expect(result.total).toBe(2);
});
it("should throw error for invalid reporter", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createReport("invalid", target, "spam", Date.now())).toThrow();
it("ignores mutes outside the validity window", () => {
const snapshot = snapshotWith({
trustedMutes: new Map([[`user:${CREATOR}`, mutesFrom(20, NOW - 61 * DAY)]]),
});
const decision = evaluateTarget(user(CREATOR), snapshot, context());
expect(decision.evidence?.trustedMuteTotal).toBe(0);
expect(decision.visibility.effect).toBe("allow");
});
it("should throw error for invalid target", () => {
expect(() => createReport(
"1234567890123456789012345678901234567890123456789012345678901234",
null,
"spam",
Date.now()
)).toThrow();
it("keeps every mute when the window is disabled", () => {
const result = aggregateMutes(
"k",
{ ...snapshotWith(), trustedMutes: new Map([["k", mutesFrom(3, NOW - 4000 * DAY)]]) },
{},
new Set(TRUSTED),
NOW,
0,
);
expect(result.total).toBe(3);
});
});
describe("surface exceptions", () => {
const bypassPolicy = createPolicyDefinition({
id: "bypass",
version: "1.0.0",
profiles: {
home: {
name: "home",
administrativeDeny: { visibility: "hide", interaction: "deny" },
bypassHide: true,
bypassHideCeiling: "restrict",
reports: {},
mutes: {},
},
},
});
it("should throw error for invalid category", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createReport(
"1234567890123456789012345678901234567890123456789012345678901234",
target,
"",
Date.now()
)).toThrow();
it("caps a hide at the configured ceiling", () => {
const snapshot = snapshotWith({
contributions: [{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] }],
});
it("should throw error for invalid timestamp", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createReport(
"1234567890123456789012345678901234567890123456789012345678901234",
target,
"spam",
-1
)).toThrow();
const decision = evaluateTarget(user(CREATOR), snapshot, {
surface: "home",
policyProfile: "home",
policy: bypassPolicy,
now: NOW,
});
expect(decision.visibility.effect).toBe("restrict");
expect(decision.reasons.map((r) => r.id)).toContain("surface-policy-bypass");
});
describe("createTrustedMute", () => {
it("should create a valid trusted mute", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const trustedMute = createTrustedMute(target, 5, { "mute": 5 });
expect(trustedMute).toEqual({
target,
count: 5,
categories: { "mute": 5 }
it("leaves a decision below the ceiling untouched", () => {
const decision = evaluateTarget(user(CREATOR), snapshotWith(), {
surface: "home",
policyProfile: "home",
policy: bypassPolicy,
now: NOW,
});
expect(decision.visibility.effect).toBe("allow");
expect(decision.reasons.map((r) => r.id)).not.toContain("surface-policy-bypass");
});
});
it("should throw error for invalid target", () => {
expect(() => createTrustedMute(null, 5, { "mute": 5 })).toThrow();
describe("evidence exposure", () => {
it("withholds contributor pubkeys by default", () => {
const snapshot = snapshotWith({ reports: new Map([[`user:${CREATOR}`, reportsFrom(3, "nudity")]]) });
const decision = evaluateTarget(user(CREATOR), snapshot, context());
expect(decision.evidence?.trustedReportTotal).toBe(3);
expect(decision.evidence?.trustedReporterPubkeys).toEqual([]);
});
it("should throw error for invalid count", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createTrustedMute(target, -1, { "mute": 5 })).toThrow();
it("exposes them when the profile opts in", () => {
const snapshot = snapshotWith({
contributions: [{ actor: ROOT, kind: "user-allow", targets: [user(CREATOR)] }],
reports: new Map([[`user:${CREATOR}`, reportsFrom(3, "nudity")]]),
});
it("should throw error for invalid categories", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createTrustedMute(target, 5, null)).toThrow();
const decision = evaluateTarget(user(CREATOR), snapshot, context("strict"));
expect(decision.evidence?.trustedReporterPubkeys).toHaveLength(3);
});
});
it("should throw error for invalid category names", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createTrustedMute(target, 5, { "": 5 })).toThrow();
describe("purity and determinism", () => {
it("produces identical decisions for identical inputs", () => {
const snapshot = snapshotWith({ reports: new Map([[`user:${CREATOR}`, reportsFrom(3, "spam")]]) });
expect(evaluateTarget(user(CREATOR), snapshot, context())).toEqual(
evaluateTarget(user(CREATOR), snapshot, context()),
);
});
it("should throw error for invalid category counts", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createTrustedMute(target, 5, { "mute": -1 })).toThrow();
it("does not mutate the snapshot", () => {
const snapshot = snapshotWith({ reports: new Map([[`user:${CREATOR}`, reportsFrom(3, "spam")]]) });
const serialize = () =>
JSON.stringify({
deny: [...snapshot.admin.userDeny],
reports: [...(snapshot.reports ?? new Map()).entries()],
});
const before = serialize();
evaluateTarget(user(CREATOR), snapshot, context());
expect(serialize()).toBe(before);
});
describe("createOverride", () => {
it("should create a valid override", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const override = createOverride(target, "hide", "Administrative override");
it("does not mutate the target", () => {
const target = user(CREATOR);
evaluateTarget(target, snapshotWith(), context());
expect(target).toEqual({ type: "user", pubkey: CREATOR });
});
expect(override).toEqual({
target,
visibility: "hide",
reason: "Administrative override"
});
it("changes the fingerprint when the snapshot changes", () => {
const clean = evaluateTarget(user(CREATOR), snapshotWith(), context());
const denied = evaluateTarget(
user(CREATOR),
snapshotWith({ contributions: [{ actor: MODERATOR, kind: "user-deny", targets: [user(CREATOR)] }] }),
context(),
);
expect(denied.snapshotFingerprint).not.toBe(clean.snapshotFingerprint);
});
it("should create an override without reason", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
const override = createOverride(target, "hide");
it("keeps the fingerprint stable across viewers", () => {
const snapshot = snapshotWith();
const a = evaluateTarget(user(CREATOR), snapshot, context(), createViewerState({ blocks: new Set() }));
const b = evaluateTarget(
user(CREATOR),
snapshot,
context(),
createViewerState({ blocks: new Set([CREATOR]) }),
);
expect(a.snapshotFingerprint).toBe(b.snapshotFingerprint);
});
});
expect(override).toEqual({
target,
visibility: "hide"
describe("evaluateMany", () => {
it("evaluates every target and keys results", () => {
const results = evaluateMany([user(CREATOR), user(STRANGER)], snapshotWith(), context());
expect(results.size).toBe(2);
expect(results.get(`user:${CREATOR}`)?.visibility.effect).toBe("allow");
});
it("skips invalid targets rather than throwing", () => {
const results = evaluateMany(
[user(CREATOR), /** @type {any} */ ({ type: "user", pubkey: "bad" })],
snapshotWith(),
context(),
);
expect(results.size).toBe(1);
});
it("should throw error for invalid target", () => {
expect(() => createOverride(null, "hide")).toThrow();
it("deduplicates repeated targets", () => {
const results = evaluateMany([user(CREATOR), user(CREATOR)], snapshotWith(), context());
expect(results.size).toBe(1);
});
it("should throw error for invalid visibility", () => {
const target = createUserTarget("abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789");
expect(() => createOverride(target, /** @type {any} */ ("invalid"))).toThrow();
});
it("agrees with evaluateTarget", () => {
const snapshot = snapshotWith({ reports: new Map([[`user:${CREATOR}`, reportsFrom(3, "spam")]]) });
const many = evaluateMany([user(CREATOR)], snapshot, context()).get(`user:${CREATOR}`);
expect(many).toEqual(evaluateTarget(user(CREATOR), snapshot, context()));
});
});
+73
View File
@@ -0,0 +1,73 @@
import { describe, expect, it } from "vitest";
import { createEmptyEvidence, freezeEvidence, redactEvidence } from "../src/evidence.js";
const populated = () => ({
...createEmptyEvidence(),
trustedReportTotal: 3,
trustedReportsByCategory: { spam: 2, nudity: 1 },
trustedReporterPubkeys: ["cc", "aa", "bb"],
trustedMuteTotal: 2,
trustedMutesByCategory: { other: 2 },
trustedMuterPubkeys: ["zz", "yy"],
});
describe("createEmptyEvidence", () => {
it("starts at zero with no flags set", () => {
const evidence = createEmptyEvidence();
expect(evidence.trustedReportTotal).toBe(0);
expect(evidence.personalBlock).toBe(false);
expect(evidence.protectedTarget).toBe(false);
expect(evidence.trustedReporterPubkeys).toEqual([]);
});
it("returns a fresh object each call", () => {
const first = createEmptyEvidence();
first.trustedReportTotal = 9;
expect(createEmptyEvidence().trustedReportTotal).toBe(0);
});
});
describe("freezeEvidence", () => {
it("sorts pubkey lists for stable serialization", () => {
expect(freezeEvidence(populated()).trustedReporterPubkeys).toEqual(["aa", "bb", "cc"]);
});
it("sorts category keys", () => {
expect(Object.keys(freezeEvidence(populated()).trustedReportsByCategory)).toEqual([
"nudity",
"spam",
]);
});
it("preserves counts", () => {
const frozen = freezeEvidence(populated());
expect(frozen.trustedReportTotal).toBe(3);
expect(frozen.trustedReportsByCategory.spam).toBe(2);
});
it("does not mutate its input", () => {
const evidence = populated();
freezeEvidence(evidence);
expect(evidence.trustedReporterPubkeys).toEqual(["cc", "aa", "bb"]);
});
it("is stable across repeated calls", () => {
expect(freezeEvidence(populated())).toEqual(freezeEvidence(populated()));
});
});
describe("redactEvidence", () => {
it("drops contributor pubkeys", () => {
const redacted = redactEvidence(populated());
expect(redacted.trustedReporterPubkeys).toEqual([]);
expect(redacted.trustedMuterPubkeys).toEqual([]);
});
it("keeps the counts that explain the decision", () => {
const redacted = redactEvidence(populated());
expect(redacted.trustedReportTotal).toBe(3);
expect(redacted.trustedMuteTotal).toBe(2);
expect(redacted.trustedReportsByCategory).toEqual({ nudity: 1, spam: 2 });
});
});
+94
View File
@@ -0,0 +1,94 @@
import { describe, expect, it } from "vitest";
import {
canonicalStringify,
fingerprint,
hashString,
snapshotFingerprint,
} from "../src/fingerprint.js";
describe("canonicalStringify", () => {
it("sorts object keys at every depth", () => {
expect(canonicalStringify({ b: 1, a: { d: 2, c: 3 } })).toBe(
canonicalStringify({ a: { c: 3, d: 2 }, b: 1 }),
);
});
it("preserves array order", () => {
expect(canonicalStringify([1, 2])).not.toBe(canonicalStringify([2, 1]));
});
it("serializes Sets order-independently", () => {
expect(canonicalStringify(new Set(["b", "a"]))).toBe(canonicalStringify(new Set(["a", "b"])));
});
it("serializes Maps order-independently", () => {
const a = new Map([["x", 1], ["y", 2]]);
const b = new Map([["y", 2], ["x", 1]]);
expect(canonicalStringify(a)).toBe(canonicalStringify(b));
});
it("skips undefined values", () => {
expect(canonicalStringify({ a: 1, b: undefined })).toBe(canonicalStringify({ a: 1 }));
});
it("handles primitives and null", () => {
expect(canonicalStringify(null)).toBe("null");
expect(canonicalStringify(5)).toBe("5");
expect(canonicalStringify("x")).toBe('"x"');
});
});
describe("hashString", () => {
it("returns a 16-character hex digest", () => {
expect(hashString("anything")).toMatch(/^[0-9a-f]{16}$/);
});
it("is stable across calls", () => {
expect(hashString("stable")).toBe(hashString("stable"));
});
it("separates similar inputs", () => {
expect(hashString("a")).not.toBe(hashString("b"));
expect(hashString("ab")).not.toBe(hashString("ba"));
});
it("handles the empty string", () => {
expect(hashString("")).toMatch(/^[0-9a-f]{16}$/);
});
});
describe("fingerprint", () => {
it("matches for structurally equal values", () => {
expect(fingerprint({ a: 1, b: 2 })).toBe(fingerprint({ b: 2, a: 1 }));
});
it("differs when a value changes", () => {
expect(fingerprint({ a: 1 })).not.toBe(fingerprint({ a: 2 }));
});
});
describe("snapshotFingerprint", () => {
it("ignores fields outside the snapshot", () => {
const base = { authority: { root: "x" }, admin: { userDeny: new Set(["a"]) } };
expect(snapshotFingerprint(base)).toBe(
snapshotFingerprint(/** @type {any} */ ({ ...base, viewer: "someone" })),
);
});
it("changes when administrative state changes", () => {
const before = snapshotFingerprint({ admin: { userDeny: new Set() } });
const after = snapshotFingerprint({ admin: { userDeny: new Set(["user:a"]) } });
expect(before).not.toBe(after);
});
it("changes when the policy changes", () => {
expect(snapshotFingerprint({ policy: { version: "1" } })).not.toBe(
snapshotFingerprint({ policy: { version: "2" } }),
);
});
it("tolerates a fully empty snapshot", () => {
expect(snapshotFingerprint({})).toMatch(/^[0-9a-f]{16}$/);
});
});
+284 -394
View File
@@ -1,428 +1,318 @@
import { describe, it, expect } from "vitest";
import { describe, expect, it } from "vitest";
import {
BUILTIN_POLICY_PROFILES,
createPolicyEffect,
createPolicyProfile,
INTERACTION_LADDER,
NEUTRAL_POLICY,
RANKING_LADDER,
TRANSACTION_LADDER,
VISIBILITY_LADDER,
applyEffects,
applyThresholds,
composeDecisions,
createNeutralDecision,
createPolicyDefinition,
createPolicyContext,
createGovernanceDecision,
getPolicyEffect,
composeGovernanceDecisions
effectSeverity,
maxEffect,
normalizeCategoryThresholds,
normalizeDimensionEffects,
normalizePolicyDefinition,
resolveProfile,
resolveThresholds,
} from "../src/policy.js";
import { createReason } from "../src/reasons.js";
describe("policy", () => {
describe("BUILTIN_POLICY_PROFILES", () => {
it("should define built-in policy profiles with effects", () => {
expect(BUILTIN_POLICY_PROFILES).toHaveProperty("default");
expect(BUILTIN_POLICY_PROFILES).toHaveProperty("moderate");
expect(BUILTIN_POLICY_PROFILES).toHaveProperty("strict");
expect(BUILTIN_POLICY_PROFILES).toHaveProperty("commerce-default");
expect(BUILTIN_POLICY_PROFILES).toHaveProperty("commerce-transaction");
describe("effect ladders", () => {
it("orders visibility from permissive to severe", () => {
expect(VISIBILITY_LADDER).toEqual(["allow", "warn", "restrict", "hide", "deny"]);
});
// Check that each profile has a name and effects
for (const [name, profile] of Object.entries(BUILTIN_POLICY_PROFILES)) {
expect(profile.name).toBe(name);
expect(profile.effects).toBeDefined();
expect(typeof profile.effects).toBe("object");
}
it("orders the remaining dimensions", () => {
expect(RANKING_LADDER).toEqual(["normal", "downrank"]);
expect(INTERACTION_LADDER).toEqual(["allow", "require-explicit-action", "deny"]);
expect(TRANSACTION_LADDER).toEqual(["allow", "require-review", "deny"]);
});
it("ranks effects by severity", () => {
expect(effectSeverity("visibility", "allow")).toBeLessThan(effectSeverity("visibility", "hide"));
expect(effectSeverity("visibility", "hide")).toBeLessThan(effectSeverity("visibility", "deny"));
});
it("returns -1 for effects outside the ladder", () => {
expect(effectSeverity("visibility", "banish")).toBe(-1);
});
it("throws for an unknown dimension", () => {
expect(() => effectSeverity(/** @type {any} */ ("vibes"), "allow")).toThrow(/Unknown decision dimension/);
});
it("keeps the more severe effect", () => {
expect(maxEffect("visibility", "allow", "hide")).toBe("hide");
expect(maxEffect("visibility", "deny", "warn")).toBe("deny");
expect(maxEffect("interaction", "allow", "require-explicit-action")).toBe("require-explicit-action");
});
it("falls back to the valid effect when one side is unknown", () => {
expect(maxEffect("visibility", "nonsense", "warn")).toBe("warn");
expect(maxEffect("visibility", "warn", "nonsense")).toBe("warn");
});
});
describe("normalizeDimensionEffects", () => {
it("passes valid effects through", () => {
expect(normalizeDimensionEffects({ visibility: "hide", interaction: "deny" })).toEqual({
visibility: "hide",
interaction: "deny",
});
});
describe("createPolicyEffect", () => {
it("should create a valid policy effect", () => {
const effect = createPolicyEffect("hide", "User is blocked", { details: "more info" });
expect(effect).toEqual({
effect: "hide",
reason: "User is blocked",
details: { details: "more info" }
});
});
it("should create a policy effect without optional fields", () => {
const effect = createPolicyEffect("allow");
expect(effect).toEqual({ effect: "allow" });
});
it("should throw error for invalid effect", () => {
expect(() => createPolicyEffect(/** @type {any} */ ("invalid"))).toThrow();
expect(() => createPolicyEffect(/** @type {any} */ (123))).toThrow();
});
});
describe("createPolicyProfile", () => {
it("should create a valid policy profile", () => {
const profile = createPolicyProfile("test-profile", {
"visibility": { effect: "hide", reason: "Hidden" },
"interaction": { effect: "deny", reason: "Denied" }
});
expect(profile).toEqual({
name: "test-profile",
effects: {
"visibility": { effect: "hide", reason: "Hidden" },
"interaction": { effect: "deny", reason: "Denied" }
}
});
});
it("should throw error for invalid profile name", () => {
expect(() => createPolicyProfile("", {})).toThrow();
expect(() => createPolicyProfile(/** @type {any} */ (123), {})).toThrow();
});
it("should throw error for invalid effects", () => {
expect(() => createPolicyProfile("test-profile", /** @type {any} */ (null))).toThrow();
expect(() => createPolicyProfile("test-profile", /** @type {any} */ ("not-an-object"))).toThrow();
});
it("should throw error for invalid effect types", () => {
expect(() => createPolicyProfile("test-profile", {
"": { effect: "allow" }
})).toThrow();
expect(() => createPolicyProfile("test-profile", {
"visibility": /** @type {any} */ (null)
})).toThrow();
expect(() => createPolicyProfile("test-profile", {
"visibility": { effect: /** @type {any} */ ("invalid") }
})).toThrow();
});
});
describe("createPolicyDefinition", () => {
it("should create a valid policy definition", () => {
const definition = createPolicyDefinition(
"test-policy",
"Test Policy",
"A test policy",
{
"default": {
name: "default",
effects: {
"visibility": { effect: "allow" }
}
}
}
it("rejects an effect from the wrong ladder", () => {
expect(() => normalizeDimensionEffects({ interaction: /** @type {any} */ ("hide") })).toThrow(
/invalid interaction effect/,
);
});
expect(definition).toEqual({
id: "test-policy",
name: "Test Policy",
description: "A test policy",
profiles: {
"default": {
name: "default",
effects: {
"visibility": { effect: "allow" }
}
}
}
it("returns an empty object for missing input", () => {
expect(normalizeDimensionEffects(/** @type {any} */ (undefined))).toEqual({});
});
});
describe("normalizeCategoryThresholds", () => {
it("lowercases categories and floors values", () => {
expect(normalizeCategoryThresholds({ SPAM: { hide: 5.7 } })).toEqual({ spam: { hide: 5 } });
});
it("drops non-numeric gates", () => {
expect(normalizeCategoryThresholds({ spam: { hide: /** @type {any} */ ("many") } })).toEqual({
spam: {},
});
});
it("should throw error for invalid ID", () => {
expect(() => createPolicyDefinition("", "Test Policy", "A test policy", {})).toThrow();
expect(() => createPolicyDefinition(/** @type {any} */ (123), "Test Policy", "A test policy", {})).toThrow();
it("rejects negative thresholds", () => {
expect(() => normalizeCategoryThresholds({ spam: { hide: -1 } })).toThrow(/must be >= 0/);
});
});
it("should throw error for invalid name", () => {
expect(() => createPolicyDefinition("test-policy", "", "A test policy", {})).toThrow();
expect(() => createPolicyDefinition("test-policy", /** @type {any} */ (123), "A test policy", {})).toThrow();
});
it("should throw error for invalid description", () => {
expect(() => createPolicyDefinition("test-policy", "Test Policy", /** @type {any} */ (123), {})).toThrow();
});
it("should throw error for invalid profiles", () => {
expect(() => createPolicyDefinition("test-policy", "Test Policy", "A test policy", /** @type {any} */ (null))).toThrow();
expect(() => createPolicyDefinition("test-policy", "Test Policy", "A test policy", /** @type {any} */ ("not-an-object"))).toThrow();
});
it("should throw error for invalid profile names", () => {
expect(() => createPolicyDefinition(
"test-policy",
"Test Policy",
"A test policy",
{
"": {
name: "",
effects: {}
}
}
)).toThrow();
});
it("should throw error for profile name mismatch", () => {
expect(() => createPolicyDefinition(
"test-policy",
"Test Policy",
"A test policy",
{
"default": {
name: "different-name",
effects: {}
}
}
)).toThrow();
});
});
describe("createPolicyContext", () => {
it("should create a valid policy context", () => {
const context = createPolicyContext("feed", "default");
expect(context).toEqual({
surface: "feed",
policyProfile: "default"
});
});
it("should create a policy context with enforcement options", () => {
const context = createPolicyContext("feed", "default", {
hardHide: true,
allowOverrides: false
});
expect(context).toEqual({
surface: "feed",
policyProfile: "default",
enforcement: {
hardHide: true,
allowOverrides: false
}
});
});
it("should throw error for invalid surface", () => {
expect(() => createPolicyContext("", "default")).toThrow();
expect(() => createPolicyContext(/** @type {any} */ (123), "default")).toThrow();
});
it("should throw error for invalid policy profile", () => {
expect(() => createPolicyContext("feed", "")).toThrow();
expect(() => createPolicyContext("feed", /** @type {any} */ (123))).toThrow();
});
});
describe("createGovernanceDecision", () => {
it("should create a valid governance decision", () => {
const decision = createGovernanceDecision(
{ effect: "hide", reason: "Hidden" },
{ effect: "deny", reason: "Denied" },
{ effect: "deny", reason: "Transaction denied" },
["reason1", "reason2"],
[{ evidence: "evidence1" }, { evidence: "evidence2" }],
{ metadata: "data" }
);
expect(decision).toEqual({
visibility: { effect: "hide", reason: "Hidden" },
interaction: { effect: "deny", reason: "Denied" },
transaction: { effect: "deny", reason: "Transaction denied" },
reasons: ["reason1", "reason2"],
evidence: [{ evidence: "evidence1" }, { evidence: "evidence2" }],
metadata: { metadata: "data" }
});
});
it("should create a governance decision without optional fields", () => {
const decision = createGovernanceDecision(
{ effect: "allow" },
{ effect: "allow" }
);
expect(decision).toEqual({
visibility: { effect: "allow" },
interaction: { effect: "allow" },
reasons: [],
evidence: []
});
});
it("should throw error for invalid visibility effect", () => {
expect(() => createGovernanceDecision(
/** @type {any} */ ({ effect: "invalid" }),
{ effect: "allow" }
)).toThrow();
});
it("should throw error for invalid interaction effect", () => {
expect(() => createGovernanceDecision(
{ effect: "allow" },
/** @type {any} */ ({ effect: "invalid" })
)).toThrow();
});
it("should throw error for invalid transaction effect", () => {
expect(() => createGovernanceDecision(
{ effect: "allow" },
{ effect: "allow" },
/** @type {any} */ ({ effect: "invalid" })
)).toThrow();
});
it("should throw error for invalid reasons", () => {
expect(() => createGovernanceDecision(
{ effect: "allow" },
{ effect: "allow" },
undefined,
/** @type {any} */ ("not-an-array")
)).toThrow();
});
it("should throw error for invalid evidence", () => {
expect(() => createGovernanceDecision(
{ effect: "allow" },
{ effect: "allow" },
undefined,
[],
/** @type {any} */ ("not-an-array")
)).toThrow();
});
});
describe("getPolicyEffect", () => {
it("should return effect for built-in profiles", () => {
const effect = getPolicyEffect("strict", "visibility");
expect(effect).toEqual({ effect: "hide" });
});
it("should return effect for custom policies", () => {
const customPolicies = {
"custom-policy": {
id: "custom-policy",
name: "Custom Policy",
description: "A custom policy",
profiles: {
"custom-profile": {
name: "custom-profile",
effects: {
"visibility": { effect: "hide", reason: "Custom hidden" }
}
}
}
}
describe("normalizePolicyDefinition", () => {
const valid = {
id: "app",
version: "1.0.0",
profiles: { feed: { name: "feed" } },
};
const effect = getPolicyEffect("custom-profile", "visibility", /** @type {any} */ (customPolicies));
expect(effect).toEqual({ effect: "hide", reason: "Custom hidden" });
it("normalizes a minimal policy", () => {
const policy = normalizePolicyDefinition(valid);
expect(policy.id).toBe("app");
expect(policy.defaultProfile).toBe("feed");
expect(policy.profiles.feed.allowViewerOverride).toBe(true);
});
it("should prefer custom policies over built-in profiles", () => {
const customPolicies = {
"custom-policy": {
id: "custom-policy",
name: "Custom Policy",
description: "A custom policy",
profiles: {
"default": {
name: "default",
effects: {
"visibility": { effect: "hide", reason: "Custom default" }
}
}
}
}
};
const effect = getPolicyEffect("default", "visibility", /** @type {any} */ (customPolicies));
expect(effect).toEqual({ effect: "hide", reason: "Custom default" });
it("requires an id and a version", () => {
expect(() => normalizePolicyDefinition({ ...valid, id: "" })).toThrow(/non-empty id/);
expect(() => normalizePolicyDefinition({ ...valid, version: "" })).toThrow(/version string/);
});
it("should return default allow effect for unknown profiles/effects", () => {
const effect = getPolicyEffect("unknown-profile", "unknown-effect");
expect(effect).toEqual({ effect: "allow" });
});
it("requires at least one profile", () => {
expect(() => normalizePolicyDefinition({ ...valid, profiles: {} })).toThrow(/at least one profile/);
});
describe("composeGovernanceDecisions", () => {
it("should return default allow decision for empty array", () => {
const decision = composeGovernanceDecisions([]);
expect(decision).toEqual({
visibility: { effect: "allow" },
interaction: { effect: "allow" },
reasons: [],
evidence: []
});
});
it("should return the single decision for array with one element", () => {
const inputDecision = createGovernanceDecision(
{ effect: "hide", reason: "Hidden" },
{ effect: "deny", reason: "Denied" },
undefined,
["reason1"],
[{ evidence: "evidence1" }]
it("rejects a default profile that does not exist", () => {
expect(() => normalizePolicyDefinition({ ...valid, defaultProfile: "nope" })).toThrow(
/is not defined/,
);
const decision = composeGovernanceDecisions([inputDecision]);
expect(decision).toEqual(inputDecision);
});
it("should compose multiple decisions with most restrictive effects", () => {
const decisions = [
createGovernanceDecision(
{ effect: "allow" },
{ effect: "allow" }
),
createGovernanceDecision(
{ effect: "hide", reason: "Hidden" },
{ effect: "deny", reason: "Denied" }
),
createGovernanceDecision(
{ effect: "restrict", reason: "Restricted" },
{ effect: "allow" }
)
];
const decision = composeGovernanceDecisions(decisions);
expect(decision.visibility).toEqual({ effect: "hide", reason: "Hidden" });
expect(decision.interaction).toEqual({ effect: "deny", reason: "Denied" });
it("names a profile from its map key when the profile omits a name", () => {
const policy = normalizePolicyDefinition({
...valid,
profiles: { checkout: /** @type {any} */ ({}) },
});
expect(policy.profiles.checkout.name).toBe("checkout");
});
it("should combine reasons and evidence from all decisions", () => {
const decisions = [
createGovernanceDecision(
{ effect: "allow" },
{ effect: "allow" },
undefined,
["reason1", "reason2"],
[{ evidence: "evidence1" }]
),
createGovernanceDecision(
{ effect: "hide" },
{ effect: "deny" },
undefined,
["reason2", "reason3"],
[{ evidence: "evidence2" }]
)
];
const decision = composeGovernanceDecisions(decisions);
expect(decision.reasons).toEqual(["reason1", "reason2", "reason3"]);
expect(decision.evidence).toEqual([
{ evidence: "evidence1" },
{ evidence: "evidence2" }
]);
it("defaults the hide ceiling when bypassHide is set", () => {
const policy = normalizePolicyDefinition({
...valid,
profiles: { feed: { name: "feed", bypassHide: true } },
});
expect(policy.profiles.feed.bypassHideCeiling).toBe("restrict");
});
it("should compose transaction effects when present", () => {
const decisions = [
createGovernanceDecision(
{ effect: "allow" },
{ effect: "allow" },
{ effect: "allow" }
),
createGovernanceDecision(
{ effect: "hide" },
{ effect: "deny" },
{ effect: "deny", reason: "Transaction denied" }
)
];
it("rejects a negative mute window", () => {
expect(() =>
normalizePolicyDefinition({
...valid,
profiles: { feed: { name: "feed", muteWindowSeconds: -5 } },
}),
).toThrow(/muteWindowSeconds/);
});
const decision = composeGovernanceDecisions(decisions);
expect(decision.transaction).toEqual({ effect: "deny", reason: "Transaction denied" });
it("is exposed as createPolicyDefinition", () => {
expect(createPolicyDefinition(valid).id).toBe("app");
});
});
describe("resolveProfile", () => {
const policy = createPolicyDefinition({
id: "app",
version: "1.0.0",
defaultProfile: "feed",
profiles: { feed: { name: "feed" }, checkout: { name: "checkout" } },
});
it("returns the requested profile", () => {
expect(resolveProfile(policy, { surface: "x", policyProfile: "checkout" }).name).toBe("checkout");
});
it("falls back to the default profile", () => {
expect(resolveProfile(policy, { surface: "x" }).name).toBe("feed");
});
it("throws for an unknown profile rather than silently falling back", () => {
expect(() => resolveProfile(policy, { surface: "x", policyProfile: "ghost" })).toThrow(
/Unknown policy profile/,
);
});
});
describe("resolveThresholds", () => {
const table = { spam: { hide: 5 }, default: { restrict: 3 } };
it("prefers an exact category match", () => {
expect(resolveThresholds(table, "spam")).toEqual({ hide: 5 });
});
it("falls back to the default entry", () => {
expect(resolveThresholds(table, "nudity")).toEqual({ restrict: 3 });
});
it("returns an empty object when nothing matches", () => {
expect(resolveThresholds({ spam: { hide: 5 } }, "nudity")).toEqual({});
expect(resolveThresholds(undefined, "spam")).toEqual({});
});
});
describe("applyThresholds", () => {
it("fires gates at or above the threshold", () => {
const { effects } = applyThresholds(3, { restrict: 3 });
expect(effects.visibility).toBe("restrict");
});
it("does not fire below the threshold", () => {
const { effects, firedGates } = applyThresholds(2, { restrict: 3 });
expect(effects).toEqual({});
expect(firedGates).toEqual([]);
});
it("keeps the most severe gate when several fire", () => {
const { effects } = applyThresholds(10, { warn: 1, restrict: 3, hide: 5 });
expect(effects.visibility).toBe("hide");
});
it("treats a zero threshold as disabled rather than always-on", () => {
const { effects } = applyThresholds(5, { hide: 0 });
expect(effects).toEqual({});
});
it("ignores non-positive counts", () => {
expect(applyThresholds(0, { warn: 1 }).effects).toEqual({});
expect(applyThresholds(Number.NaN, { warn: 1 }).effects).toEqual({});
});
it("maps gates onto their own dimensions", () => {
const { effects } = applyThresholds(5, {
downrank: 1,
restrict: 2,
requireExplicitAction: 3,
transactionDeny: 4,
});
expect(effects).toEqual({
ranking: "downrank",
visibility: "restrict",
interaction: "require-explicit-action",
transaction: "deny",
});
});
});
describe("applyEffects", () => {
it("escalates but never softens", () => {
const decision = createNeutralDecision();
applyEffects(decision, { visibility: "hide" });
applyEffects(decision, { visibility: "warn" });
expect(decision.visibility.effect).toBe("hide");
});
it("accumulates downrank weight", () => {
const decision = createNeutralDecision();
applyEffects(decision, { ranking: "downrank" });
applyEffects(decision, { ranking: "downrank" });
expect(decision.ranking.weight).toBe(2);
expect(decision.ranking.effect).toBe("downrank");
});
it("creates the transaction dimension on demand", () => {
const decision = createNeutralDecision();
expect(decision.transaction).toBeUndefined();
applyEffects(decision, { transaction: "deny" });
expect(decision.transaction).toEqual({ effect: "deny" });
});
it("deduplicates reasons", () => {
const decision = createNeutralDecision();
applyEffects(decision, {}, [createReason("viewer-block"), createReason("viewer-block")]);
expect(decision.reasons).toHaveLength(1);
});
});
describe("composeDecisions", () => {
const withVisibility = (effect, weight = 0) => {
const decision = createNeutralDecision();
decision.visibility.effect = effect;
decision.ranking.weight = weight;
return decision;
};
it("returns a neutral decision for an empty list", () => {
const composed = composeDecisions([]);
expect(composed.visibility.effect).toBe("allow");
});
it("keeps the most severe effect", () => {
expect(composeDecisions([withVisibility("warn"), withVisibility("hide")]).visibility.effect).toBe(
"hide",
);
});
it("is order-independent", () => {
const a = composeDecisions([withVisibility("warn"), withVisibility("hide")]);
const b = composeDecisions([withVisibility("hide"), withVisibility("warn")]);
expect(a.visibility.effect).toBe(b.visibility.effect);
});
it("sums ranking weight", () => {
expect(composeDecisions([withVisibility("allow", 2), withVisibility("allow", 3)]).ranking.weight).toBe(5);
});
it("makes a decision non-overridable if any input was", () => {
const locked = createNeutralDecision();
locked.visibility.overridable = false;
expect(composeDecisions([createNeutralDecision(), locked]).visibility.overridable).toBe(false);
});
it("does not mutate its inputs", () => {
const first = withVisibility("warn");
composeDecisions([first, withVisibility("deny")]);
expect(first.visibility.effect).toBe("warn");
});
});
describe("NEUTRAL_POLICY", () => {
it("enforces administrative denial", () => {
expect(NEUTRAL_POLICY.profiles.default.administrativeDeny?.visibility).toBe("hide");
});
it("applies no trust thresholds of its own", () => {
expect(NEUTRAL_POLICY.profiles.default.reports).toEqual({});
expect(NEUTRAL_POLICY.profiles.default.mutes).toEqual({});
});
});
+76
View File
@@ -0,0 +1,76 @@
import { describe, expect, it } from "vitest";
import { REASON_IDS, createReason, dedupeReasons, isReasonId } from "../src/reasons.js";
describe("reason identifiers", () => {
it("exposes the documented set", () => {
expect(REASON_IDS).toContain("viewer-block");
expect(REASON_IDS).toContain("trusted-mute-threshold");
expect(REASON_IDS).toContain("surface-policy-bypass");
});
it("has no duplicates", () => {
expect(new Set(REASON_IDS).size).toBe(REASON_IDS.length);
});
it("recognizes known ids only", () => {
expect(isReasonId("viewer-block")).toBe(true);
expect(isReasonId("made-up")).toBe(false);
expect(isReasonId(undefined)).toBe(false);
});
});
describe("createReason", () => {
it("creates a bare reason", () => {
expect(createReason("viewer-block")).toEqual({ id: "viewer-block" });
});
it("attaches detail fields", () => {
expect(
createReason("trusted-report-threshold", { category: " Spam ", count: 4, threshold: 3 }),
).toEqual({ id: "trusted-report-threshold", category: "Spam", count: 4, threshold: 3 });
});
it("omits blank and non-numeric detail", () => {
expect(createReason("trusted-report", { category: " ", count: Number.NaN })).toEqual({
id: "trusted-report",
});
});
it("rejects an unknown identifier", () => {
expect(() => createReason(/** @type {any} */ ("invented"))).toThrow(/Unknown reason identifier/);
});
});
describe("dedupeReasons", () => {
it("collapses identical reasons", () => {
expect(dedupeReasons([createReason("viewer-block"), createReason("viewer-block")])).toHaveLength(1);
});
it("keeps reasons that differ by category", () => {
const reasons = dedupeReasons([
createReason("trusted-report", { category: "spam" }),
createReason("trusted-report", { category: "nudity" }),
]);
expect(reasons).toHaveLength(2);
});
it("keeps the highest count when merging", () => {
const reasons = dedupeReasons([
createReason("trusted-report", { count: 2 }),
createReason("trusted-report", { count: 7 }),
]);
expect(reasons[0].count).toBe(7);
});
it("preserves first-seen order", () => {
const reasons = dedupeReasons([createReason("viewer-mute"), createReason("viewer-block")]);
expect(reasons.map((r) => r.id)).toEqual(["viewer-mute", "viewer-block"]);
});
it("drops malformed entries", () => {
expect(
dedupeReasons(/** @type {any[]} */ ([null, { id: "not-real" }, createReason("viewer-block")])),
).toHaveLength(1);
});
});
+13
View File
@@ -1,8 +1,21 @@
{
"name": "@nostr-governance/testing",
"version": "0.1.0",
"type": "module",
"private": true,
"main": "src/index.js",
"exports": {
".": {
"import": "./src/index.js"
},
"./conformance": {
"import": "./src/conformance.js"
}
},
"scripts": {
"test": "vitest run"
},
"dependencies": {
"@nostr-governance/core": "*"
}
}
+193 -20
View File
@@ -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 };
}
+145 -33
View File
@@ -1,42 +1,154 @@
// Test the conformance utilities
import { describe, it, expect } from "vitest";
import { createFixture, runConformanceTest } from "../src/conformance.js";
import { describe, expect, it } from "vitest";
describe("conformance utilities", () => {
it("creates valid fixture objects", () => {
const fixture = createFixture(
"test-fixture",
{
adminState: { editors: [] },
trust: {},
reports: [],
trustedMutes: [],
overrides: {},
import { createPolicyDefinition } from "@nostr-governance/core";
import { buildSnapshot, diffExpectation, evaluateFixture, runConformanceCase } from "../src/conformance.js";
const ROOT = "a1".repeat(32);
const MODERATOR = "b2".repeat(32);
const CREATOR = "d4".repeat(32);
const TRUSTED = "01".repeat(32);
const POLICY = createPolicyDefinition({
id: "harness",
version: "1.0.0",
defaultProfile: "feed",
profiles: {
feed: {
name: "feed",
administrativeDeny: { visibility: "hide", interaction: "deny" },
reports: { default: { restrict: 1 } },
mutes: {},
},
{ type: "user", pubkey: "test" },
{ visibility: "allow" }
);
},
});
expect(fixture.name).toBe("test-fixture");
expect(fixture.state.adminState.editors).toEqual([]);
expect(fixture.target).toEqual({ type: "user", pubkey: "test" });
expect(fixture.expectation).toEqual({ visibility: "allow" });
const baseFixture = () => ({
name: "sample",
profile: "feed",
now: 1_750_000_000,
authority: {
root: ROOT,
actors: { [ROOT]: ["super_admin"], [MODERATOR]: ["moderator"] },
protectedActors: [ROOT],
},
target: { type: "user", pubkey: CREATOR },
expect: {
visibility: { effect: "allow" },
interaction: { effect: "allow" },
},
});
describe("buildSnapshot", () => {
it("reduces contributions through the authority state", () => {
const fixture = {
...baseFixture(),
contributions: [
{ actor: MODERATOR, kind: "user-deny", targets: [{ type: "user", pubkey: CREATOR }] },
],
};
const { snapshot } = buildSnapshot(fixture);
expect(snapshot.admin.userDeny.has(`user:${CREATOR}`)).toBe(true);
});
it("runs conformance tests", () => {
const fixture = createFixture(
"test-fixture",
{},
{ type: "user", pubkey: "test" },
{ visibility: "allow" }
);
it("converts viewer arrays into sets and maps", () => {
const fixture = {
...baseFixture(),
viewer: {
blocks: [CREATOR],
mutes: [MODERATOR],
overrides: [{ key: `user:${CREATOR}`, visibility: "allow" }],
},
};
const { viewer } = buildSnapshot(fixture);
expect(viewer.blocks?.has(CREATOR)).toBe(true);
expect(viewer.mutes?.has(MODERATOR)).toBe(true);
expect(viewer.overrides?.get(`user:${CREATOR}`)).toEqual({
visibility: "allow",
reason: undefined,
});
});
// Mock evaluator that always returns the expectation
const mockEvaluator = () => ({ visibility: "allow" });
it("defaults every optional section", () => {
const { snapshot, viewer } = buildSnapshot(baseFixture());
expect(snapshot.reports?.size).toBe(0);
expect(snapshot.trustedMutes?.size).toBe(0);
expect(viewer.blocks?.size).toBe(0);
});
});
const result = runConformanceTest(fixture, mockEvaluator);
expect(result.passed).toBe(true);
expect(result.actual).toEqual({ visibility: "allow" });
expect(result.expected).toEqual({ visibility: "allow" });
describe("evaluateFixture", () => {
it("evaluates against the fixture's profile", () => {
const decision = evaluateFixture(baseFixture(), POLICY);
expect(decision.policyProfile).toBe("feed");
expect(decision.visibility.effect).toBe("allow");
});
});
describe("diffExpectation", () => {
const decision = () => evaluateFixture(baseFixture(), POLICY);
it("reports no mismatches when the expectation holds", () => {
expect(diffExpectation(decision(), { visibility: { effect: "allow" } })).toEqual([]);
});
it("reports a visibility mismatch", () => {
const [mismatch] = diffExpectation(decision(), { visibility: { effect: "hide" } });
expect(mismatch).toMatch(/visibility.effect: expected "hide", got "allow"/);
});
it("reports a ranking weight mismatch", () => {
const [mismatch] = diffExpectation(decision(), { ranking: { effect: "normal", weight: 3 } });
expect(mismatch).toMatch(/ranking.weight/);
});
it("compares reasons order-insensitively", () => {
const fixture = {
...baseFixture(),
contributions: [
{ actor: MODERATOR, kind: "user-deny", targets: [{ type: "user", pubkey: CREATOR }] },
],
};
const result = evaluateFixture(fixture, POLICY);
expect(diffExpectation(result, { reasons: ["admin-user-deny"] })).toEqual([]);
});
it("reports a missing transaction dimension", () => {
const [mismatch] = diffExpectation(decision(), { transaction: { effect: "deny" } });
expect(mismatch).toMatch(/\(absent\)/);
});
it("compares evidence fields", () => {
const fixture = {
...baseFixture(),
trust: { contacts: [TRUSTED] },
reports: {
[`user:${CREATOR}`]: [{ reporter: TRUSTED, category: "spam", createdAt: 1_749_000_000 }],
},
};
const result = evaluateFixture(fixture, POLICY);
expect(diffExpectation(result, { evidence: { trustedReportTotal: 1 } })).toEqual([]);
expect(diffExpectation(result, { evidence: { trustedReportTotal: 5 } })).toHaveLength(1);
});
it("compares pubkey lists by length via the Count suffix", () => {
const result = decision();
expect(diffExpectation(result, { evidence: { trustedReporterPubkeysCount: 0 } })).toEqual([]);
expect(diffExpectation(result, { evidence: { trustedReporterPubkeysCount: 2 } })).toHaveLength(1);
});
});
describe("runConformanceCase", () => {
it("passes a fixture whose expectation holds", () => {
const { passed, mismatches } = runConformanceCase(baseFixture(), POLICY);
expect(passed).toBe(true);
expect(mismatches).toEqual([]);
});
it("fails a fixture whose expectation does not hold", () => {
const fixture = { ...baseFixture(), expect: { visibility: { effect: "deny" } } };
const { passed, mismatches } = runConformanceCase(fixture, POLICY);
expect(passed).toBe(false);
expect(mismatches).toHaveLength(1);
});
});
+800
View File
@@ -0,0 +1,800 @@
#!/usr/bin/env node
/**
* Generate the characterization corpus.
*
* Each case encodes one behavior read from the pinned reference commit (see
* docs/reference-map.md) as a snapshot, a target, and an expected decision.
* The corpus is generated rather than hand-written so that pubkeys stay
* consistent across cases and a behavior change shows up as a reviewable diff.
*
* Provenance: these fixtures are authored from the reference source and its
* documented configuration, not captured from a live relay. They pin the
* behavior the extraction must reproduce; they are not a recording of
* production traffic.
*
* Usage: node scripts/build-characterization-corpus.mjs
*/
import { mkdir, writeFile, readdir, unlink } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
const CASES_DIR = join(ROOT, "fixtures", "bitvid", "cases");
/** @param {string} seed */
const key = (seed) => seed.repeat(64).slice(0, 64);
const PUBKEYS = {
root: key("a1"),
moderator: key("b2"),
curator: key("c3"),
creator: key("d4"),
viewer: key("e5"),
stranger: key("f6"),
};
/**
* Trusted accounts used to reach report and mute thresholds.
* Each index gets its own two-hex-digit seed so that counting cases which need
* twenty distinct muters actually get twenty distinct pubkeys.
*/
const trusted = (index) => key(index.toString(16).padStart(2, "0"));
const TRUSTED = Array.from({ length: 24 }, (_, index) => trusted(index + 1));
const EVENT_ID = key("1b");
const OTHER_EVENT_ID = key("2c");
const DAY = 24 * 60 * 60;
const NOW = 1_750_000_000;
const userTarget = (pubkey) => ({ type: "user", pubkey });
const eventTarget = (id, author) => ({ type: "event", id, author });
const addressTarget = (kind, pubkey, identifier) => ({ type: "address", kind, pubkey, identifier });
/** Authority roster shared by every case. */
const AUTHORITY = {
root: PUBKEYS.root,
actors: {
[PUBKEYS.root]: ["super_admin"],
[PUBKEYS.moderator]: ["moderator"],
[PUBKEYS.curator]: ["curator"],
},
protectedActors: [PUBKEYS.root],
};
/** Reports from N distinct trusted accounts in one category. */
const reportsFrom = (count, category, createdAt = NOW - DAY) =>
TRUSTED.slice(0, count).map((reporter) => ({ reporter, category, createdAt }));
/** Mutes from N distinct trusted accounts. */
const mutesFrom = (count, updatedAt = NOW - DAY) =>
TRUSTED.slice(0, count).map((muter) => ({ muter, updatedAt }));
/** Every trusted account follows the creator's critics. */
const trustAll = { contacts: TRUSTED };
/**
* @typedef {Object} FixtureCase
* @property {string} name
* @property {string} description
* @property {string} behavior - What the reference implementation does
* @property {string} profile
* @property {number} now
* @property {Object} authority
* @property {Array} [contributions]
* @property {Object} [trust]
* @property {Object} [viewer]
* @property {Object} [reports]
* @property {Object} [trustedMutes]
* @property {Object} target
* @property {Object} expect
*/
/** @type {FixtureCase[]} */
const CASES = [
{
name: "trusted-report-threshold-reached",
description: "Three trusted reports restrict visibility and withhold autoplay.",
behavior: "Blur threshold and autoplay-block threshold are both 3 trusted reports.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
reports: { [`event:${EVENT_ID}`]: reportsFrom(3, "nudity") },
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "restrict", overridable: true },
interaction: { effect: "require-explicit-action" },
reasons: ["trusted-report", "trusted-report-threshold"],
},
},
{
name: "trusted-report-below-threshold",
description: "Two trusted reports are not enough to restrict.",
behavior: "Counts below the blur threshold leave the target fully visible.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
reports: { [`event:${EVENT_ID}`]: reportsFrom(2, "nudity") },
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
reasons: ["trusted-report"],
},
},
{
name: "duplicate-reporter-dedupe",
description: "One account reporting the same category repeatedly counts once.",
behavior: "Reports are deduplicated per reporter and category before counting.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
reports: {
[`event:${EVENT_ID}`]: [
{ reporter: TRUSTED[0], category: "spam", createdAt: NOW - DAY },
{ reporter: TRUSTED[0], category: "spam", createdAt: NOW - 2 * DAY },
{ reporter: TRUSTED[0], category: "spam", createdAt: NOW - 3 * DAY },
{ reporter: TRUSTED[1], category: "spam", createdAt: NOW - DAY },
],
},
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
reasons: ["trusted-report"],
evidence: { trustedReportTotal: 2 },
},
},
{
name: "blocked-reporter-ignored",
description: "Reports from accounts the viewer blocks do not count.",
behavior: "Blocked reporters are skipped before aggregation.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
viewer: { blocks: [TRUSTED[0], TRUSTED[1]] },
reports: { [`event:${EVENT_ID}`]: reportsFrom(3, "nudity") },
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
evidence: { trustedReportTotal: 1 },
},
},
{
name: "denied-reporter-ignored",
description: "Reports from administratively denied accounts do not count.",
behavior: "Denied accounts are excluded from aggregation, so a denied account cannot manufacture reports.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
contributions: [
{ actor: PUBKEYS.moderator, kind: "user-deny", targets: [userTarget(TRUSTED[0]), userTarget(TRUSTED[1])] },
],
reports: { [`event:${EVENT_ID}`]: reportsFrom(3, "nudity") },
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
evidence: { trustedReportTotal: 1 },
},
},
{
name: "untrusted-reporter-ignored",
description: "Reports from outside the trust graph do not count.",
behavior: "Only reports from the viewer's trusted graph are counted.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: { contacts: [TRUSTED[0]] },
reports: {
[`event:${EVENT_ID}`]: [
{ reporter: TRUSTED[0], category: "nudity", createdAt: NOW - DAY },
{ reporter: PUBKEYS.stranger, category: "nudity", createdAt: NOW - DAY },
],
},
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
evidence: { trustedReportTotal: 1 },
},
},
{
name: "spam-report-hide-threshold",
description: "Five trusted spam reports escalate to a hide.",
behavior: "Spam carries its own higher hide threshold; other categories do not hide on reports alone.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
reports: { [`event:${EVENT_ID}`]: reportsFrom(5, "spam") },
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "hide", overridable: true },
interaction: { effect: "require-explicit-action" },
reasons: ["trusted-report", "trusted-report-threshold"],
},
},
{
name: "non-spam-reports-never-hide",
description: "Eight trusted reports in a non-spam category restrict but never hide.",
behavior: "Only spam has a report-driven hide threshold.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
reports: { [`event:${EVENT_ID}`]: reportsFrom(8, "misleading") },
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "restrict", overridable: true },
interaction: { effect: "require-explicit-action" },
},
},
{
name: "trusted-mute-below-threshold",
description: "Nineteen trusted mutes downrank only.",
behavior: "Below the hide threshold a trusted mute is a ranking signal: no blur, no playback block, no denial.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
trustedMutes: { [`user:${PUBKEYS.creator}`]: mutesFrom(19) },
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "downrank", weight: 1 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
reasons: ["trusted-mute", "trusted-mute-threshold"],
evidence: { trustedMuteTotal: 19 },
},
},
{
name: "trusted-mute-at-threshold",
description: "Twenty trusted mutes escalate to a reversible hide.",
behavior: "The hide threshold is 20 unique trusted muters.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
trustedMutes: { [`user:${PUBKEYS.creator}`]: mutesFrom(20) },
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "downrank", weight: 1 },
visibility: { effect: "hide", overridable: true },
interaction: { effect: "allow" },
evidence: { trustedMuteTotal: 20 },
},
},
{
name: "expired-trusted-mute",
description: "Mutes older than the 60-day window are ignored.",
behavior: "Stale mute lists must not hold a target down indefinitely.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
trustedMutes: {
[`user:${PUBKEYS.creator}`]: [
...mutesFrom(20, NOW - 61 * DAY),
...mutesFrom(2, NOW - DAY).map((entry, index) => ({ ...entry, muter: TRUSTED[20 + index] })),
],
},
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "downrank", weight: 1 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
evidence: { trustedMuteTotal: 2 },
},
},
{
name: "duplicate-muter-dedupe",
description: "The same account muting repeatedly counts once.",
behavior: "Muters are counted uniquely.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
trustedMutes: {
[`user:${PUBKEYS.creator}`]: [
{ muter: TRUSTED[0], updatedAt: NOW - DAY },
{ muter: TRUSTED[0], updatedAt: NOW - 2 * DAY },
{ muter: TRUSTED[1], updatedAt: NOW - DAY },
],
},
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "downrank", weight: 1 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
evidence: { trustedMuteTotal: 2 },
},
},
{
name: "personal-block-precedence",
description: "A viewer's own block hides the target regardless of other state.",
behavior: "Viewer blocks are viewer-local and always enforced.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
viewer: { blocks: [PUBKEYS.creator] },
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "hide", overridable: true },
interaction: { effect: "deny" },
reasons: ["viewer-block"],
evidence: { personalBlock: true },
},
},
{
name: "admin-user-deny",
description: "A moderator's user denial hides the creator.",
behavior: "Administrative denial applies the profile's administrativeDeny effects.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
contributions: [
{ actor: PUBKEYS.moderator, kind: "user-deny", targets: [userTarget(PUBKEYS.creator)] },
],
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "hide", overridable: true },
interaction: { effect: "deny" },
reasons: ["admin-user-deny"],
evidence: { userDenied: true },
},
},
{
name: "admin-event-deny",
description: "An exact event denial hides only that event.",
behavior: "Event denial applies to the named event and does not travel to the author.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
contributions: [
{ actor: PUBKEYS.moderator, kind: "event-deny", targets: [eventTarget(EVENT_ID, PUBKEYS.creator)] },
],
target: eventTarget(OTHER_EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
reasons: [],
},
},
{
name: "admin-event-deny-applies",
description: "The denied event itself is hidden.",
behavior: "Exact-event denial hides the named event.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
contributions: [
{ actor: PUBKEYS.moderator, kind: "event-deny", targets: [eventTarget(EVENT_ID, PUBKEYS.creator)] },
],
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "hide", overridable: true },
interaction: { effect: "deny" },
reasons: ["admin-event-deny"],
evidence: { eventDenied: true },
},
},
{
name: "author-deny-reaches-events",
description: "Denying a creator hides that creator's events.",
behavior: "Author-level denial reaches the author's content.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
contributions: [
{ actor: PUBKEYS.moderator, kind: "user-deny", targets: [userTarget(PUBKEYS.creator)] },
],
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "hide", overridable: true },
interaction: { effect: "deny" },
reasons: ["admin-user-deny"],
evidence: { userDenied: true },
},
},
{
name: "admin-address-deny",
description: "Denying an address hides it across revisions.",
behavior: "Address denial is keyed by coordinate, so republishing does not clear it.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
contributions: [
{
actor: PUBKEYS.moderator,
kind: "address-deny",
targets: [addressTarget("30023", PUBKEYS.creator, "listing-1")],
},
],
target: addressTarget("30023", PUBKEYS.creator, "listing-1"),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "hide", overridable: true },
interaction: { effect: "deny" },
reasons: ["admin-address-deny"],
evidence: { addressDenied: true },
},
},
{
name: "community-blacklist-merge",
description: "A curator's denial is honored without moderator authority.",
behavior: "Curators hold contribute-user-deny only; their entries merge as a union.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
contributions: [
{
actor: PUBKEYS.curator,
kind: "user-deny",
source: "community-list-1",
targets: [userTarget(PUBKEYS.creator)],
},
],
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "hide", overridable: true },
interaction: { effect: "deny" },
reasons: ["admin-user-deny", "community-user-deny"],
},
},
{
name: "curator-cannot-deny-events",
description: "A curator's event denial is ignored.",
behavior: "Contributions beyond an actor's capabilities are dropped at merge time.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
contributions: [
{ actor: PUBKEYS.curator, kind: "event-deny", targets: [eventTarget(EVENT_ID, PUBKEYS.creator)] },
],
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
reasons: [],
},
},
{
name: "revoked-actor-contribution-dropped",
description: "An actor with no roles cannot deny anyone.",
behavior: "Revocation takes effect immediately because capabilities resolve at merge time.",
profile: "feed",
now: NOW,
authority: { root: PUBKEYS.root, actors: { [PUBKEYS.root]: ["super_admin"] }, protectedActors: [PUBKEYS.root] },
contributions: [
{ actor: PUBKEYS.moderator, kind: "user-deny", targets: [userTarget(PUBKEYS.creator)] },
],
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
reasons: [],
},
},
{
name: "protected-target",
description: "The root administrator cannot be denied by contributor lists.",
behavior: "Protected actors are stripped from every denial set.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
contributions: [
{ actor: PUBKEYS.moderator, kind: "user-deny", targets: [userTarget(PUBKEYS.root)] },
],
target: userTarget(PUBKEYS.root),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
evidence: { protectedTarget: true },
},
},
{
name: "moderator-trust-seed",
description: "An anonymous viewer inherits trust seeds.",
behavior: "Seeds are the fallback trust graph when the viewer follows nobody.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: { contacts: [], seeds: TRUSTED.slice(0, 3) },
reports: { [`event:${EVENT_ID}`]: reportsFrom(3, "nudity") },
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "restrict", overridable: true },
interaction: { effect: "require-explicit-action" },
evidence: { trustedReportTotal: 3 },
},
},
{
name: "seeds-yield-to-real-contacts",
description: "Once the viewer has contacts, seeds stop contributing.",
behavior: "Following nobody must not be equivalent to following the seed set.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: { contacts: [TRUSTED[0]], seeds: TRUSTED.slice(0, 3) },
reports: { [`event:${EVENT_ID}`]: reportsFrom(3, "nudity") },
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
evidence: { trustedReportTotal: 1 },
},
},
{
name: "viewer-override-softens-hide",
description: "A viewer override reveals hidden content behind an explicit action.",
behavior: "Overrides may soften a decision and force explicit interaction; they never escalate.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
viewer: { overrides: [{ key: `user:${PUBKEYS.creator}`, visibility: "allow" }] },
trustedMutes: { [`user:${PUBKEYS.creator}`]: mutesFrom(20) },
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "downrank", weight: 1 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "require-explicit-action" },
reasons: ["trusted-mute", "trusted-mute-threshold", "viewer-override"],
},
},
{
name: "viewer-override-cannot-escalate",
description: "An override asking for a harsher effect is ignored.",
behavior: "Overrides only soften.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
viewer: { overrides: [{ key: `user:${PUBKEYS.creator}`, visibility: "deny" }] },
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
reasons: [],
},
},
{
name: "author-override",
description: "An override on the author does not leak to the author's events.",
behavior: "Overrides are keyed by exact target.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
contributions: [
{ actor: PUBKEYS.moderator, kind: "user-deny", targets: [userTarget(PUBKEYS.creator)] },
],
viewer: { overrides: [{ key: `user:${PUBKEYS.creator}`, visibility: "allow" }] },
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "hide", overridable: true },
interaction: { effect: "deny" },
reasons: ["admin-user-deny"],
},
},
{
name: "home-hide-bypass",
description: "The home surface downranks instead of hiding.",
behavior: "Home and Recent decline to hard-hide so one stale list cannot empty the feed.",
profile: "home",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
trustedMutes: { [`user:${PUBKEYS.creator}`]: mutesFrom(20) },
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "downrank", weight: 1 },
visibility: { effect: "restrict", overridable: true },
interaction: { effect: "allow" },
reasons: ["trusted-mute", "trusted-mute-threshold", "surface-policy-bypass"],
},
},
{
name: "recent-hide-bypass",
description: "The recent surface applies the same exception as home.",
behavior: "Recent shares the home surface exception.",
profile: "recent",
now: NOW,
authority: AUTHORITY,
contributions: [
{ actor: PUBKEYS.moderator, kind: "user-deny", targets: [userTarget(PUBKEYS.creator)] },
],
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "restrict", overridable: true },
interaction: { effect: "deny" },
reasons: ["admin-user-deny", "surface-policy-bypass"],
},
},
{
name: "playback-enforces-hide",
description: "Playback enforces the full decision with no surface exception.",
behavior: "The hide bypass is a discovery-surface concession, not a global one.",
profile: "playback",
now: NOW,
authority: AUTHORITY,
contributions: [
{ actor: PUBKEYS.moderator, kind: "user-deny", targets: [userTarget(PUBKEYS.creator)] },
],
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "deny", overridable: true },
interaction: { effect: "deny" },
reasons: ["admin-user-deny"],
},
},
{
name: "playback-exposes-evidence",
description: "The playback surface exposes reporter pubkeys; discovery surfaces do not.",
behavior: "exposeEvidence controls whether contributor pubkeys reach the consumer.",
profile: "playback",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
reports: { [`event:${EVENT_ID}`]: reportsFrom(3, "nudity") },
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "restrict", overridable: true },
interaction: { effect: "require-explicit-action" },
evidence: { trustedReportTotal: 3, trustedReporterPubkeysCount: 3 },
},
},
{
name: "evidence-redacted-by-default",
description: "Feed surfaces report counts but not who produced them.",
behavior: "Reporter pubkeys are withheld unless the profile opts in.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
reports: { [`event:${EVENT_ID}`]: reportsFrom(3, "nudity") },
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "restrict", overridable: true },
interaction: { effect: "require-explicit-action" },
evidence: { trustedReportTotal: 3, trustedReporterPubkeysCount: 0 },
},
},
{
name: "cache-fallback-effective",
description: "State reduced from a cached snapshot decides identically to live state.",
behavior: "Administrative state stays effective when relays are unreachable.",
profile: "feed",
now: NOW,
fromCache: true,
authority: AUTHORITY,
contributions: [
{ actor: PUBKEYS.moderator, kind: "user-deny", targets: [userTarget(PUBKEYS.creator)] },
],
target: userTarget(PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "hide", overridable: true },
interaction: { effect: "deny" },
reasons: ["admin-user-deny"],
},
},
{
name: "clean-target-allowed",
description: "A target with no signals against it is fully allowed.",
behavior: "Governance is opt-in per signal; absence of evidence is not suspicion.",
profile: "feed",
now: NOW,
authority: AUTHORITY,
trust: trustAll,
target: eventTarget(EVENT_ID, PUBKEYS.creator),
expect: {
ranking: { effect: "normal", weight: 0 },
visibility: { effect: "allow", overridable: true },
interaction: { effect: "allow" },
reasons: [],
},
},
];
async function main() {
await mkdir(CASES_DIR, { recursive: true });
// Drop stale cases so a renamed fixture cannot linger and pass silently.
for (const entry of await readdir(CASES_DIR).catch(() => [])) {
if (entry.endsWith(".json")) {
await unlink(join(CASES_DIR, entry));
}
}
const names = new Set();
for (const testCase of CASES) {
if (names.has(testCase.name)) {
throw new Error(`Duplicate fixture name: ${testCase.name}`);
}
names.add(testCase.name);
await writeFile(
join(CASES_DIR, `${testCase.name}.json`),
`${JSON.stringify(testCase, null, 2)}\n`,
"utf8",
);
}
await writeFile(
join(ROOT, "fixtures", "bitvid", "characterization-cases.json"),
`${JSON.stringify(CASES.map((entry) => entry.name), null, 2)}\n`,
"utf8",
);
console.log(`✓ Wrote ${CASES.length} characterization cases to fixtures/bitvid/cases`);
}
main().catch((error) => {
console.error("build-characterization-corpus failed:", error);
process.exit(1);
});
+69 -32
View File
@@ -1,47 +1,84 @@
// Test harness for BitVid parity cases
import { describe, it, expect } from "vitest";
// Conformance suite for the characterization corpus.
//
// Every fixture under fixtures/bitvid/cases is evaluated against the reference
// application profile. A change to a threshold, a precedence rule, or an effect
// ladder must show up here as a failing case.
// BitVid parity characterization cases
const BITVID_CHARACTERIZATION_CASES = [
"personal-block-precedence",
"user-deny",
"event-deny",
"address-deny",
"allowlist-miss",
"protected-target",
import { readdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { runConformanceCase } from "@nostr-governance/testing/conformance";
import { REFERENCE_POLICY } from "@nostr-governance/bitvid-compat/profile";
const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
const CASES_DIR = join(ROOT, "fixtures", "bitvid", "cases");
const CASE_INDEX = join(ROOT, "fixtures", "bitvid", "characterization-cases.json");
const caseFiles = readdirSync(CASES_DIR).filter((name) => name.endsWith(".json")).sort();
const fixtures = caseFiles.map((name) => JSON.parse(readFileSync(join(CASES_DIR, name), "utf8")));
describe("characterization corpus", () => {
it("has fixtures on disk", () => {
expect(fixtures.length).toBeGreaterThanOrEqual(15);
});
it("index matches the files on disk", () => {
const index = JSON.parse(readFileSync(CASE_INDEX, "utf8"));
expect([...index].sort()).toEqual(fixtures.map((fixture) => fixture.name).sort());
});
it("covers every behavior the migration plan requires", () => {
const names = new Set(fixtures.map((fixture) => fixture.name));
const required = [
"trusted-report-threshold-reached",
"duplicate-reporter-dedupe",
"blocked-reporter-ignored",
"trusted-mute-below-threshold",
"trusted-mute-at-threshold",
"expired-trusted-mute",
"viewer-override",
"personal-block-precedence",
"admin-user-deny",
"admin-event-deny",
"community-blacklist-merge",
"moderator-trust-seed",
"viewer-override-softens-hide",
"author-override",
"home-hide-bypass",
"cache-fallback-effective",
"report-category-thresholds",
"mute-aggregation-rules",
"hide-vs-restrict-vs-downrank",
"moderation-decorator-fields"
];
];
for (const name of required) {
expect(names, `missing required characterization case: ${name}`).toContain(name);
}
});
});
describe("BitVid parity characterization", () => {
// This suite will grow as we add real fixtures
// For now, just ensure the harness loads correctly
describe.each(fixtures.map((fixture) => [fixture.name, fixture]))("%s", (_name, fixture) => {
it(fixture.description, () => {
const { passed, mismatches, decision } = runConformanceCase(fixture, REFERENCE_POLICY);
expect(passed, `${fixture.behavior}\n ${mismatches.join("\n ")}`).toBe(true);
expect(decision.policyProfile).toBe(fixture.profile);
expect(decision.policyVersion).toBe(REFERENCE_POLICY.version);
expect(decision.snapshotFingerprint).toMatch(/^[0-9a-f]{16}$/);
});
});
it("has a list of characterization cases", () => {
expect(Array.isArray(BITVID_CHARACTERIZATION_CASES)).toBe(true);
expect(BITVID_CHARACTERIZATION_CASES.length).toBe(19);
describe("evaluation determinism", () => {
it("produces identical decisions for identical snapshots", () => {
for (const fixture of fixtures) {
const first = runConformanceCase(fixture, REFERENCE_POLICY).decision;
const second = runConformanceCase(fixture, REFERENCE_POLICY).decision;
expect(second).toEqual(first);
expect(second.snapshotFingerprint).toBe(first.snapshotFingerprint);
}
});
// Placeholder for when we have real fixtures
describe.skip("trusted-report-threshold-reached", () => {
it("should hide targets with sufficient trusted reports", async () => {
// Load fixture
// const fixture = await import("../../fixtures/bitvid/trusted-report-threshold.fixture.js");
// Run through evaluator
// const decision = evaluate(fixture.state, fixture.target);
// Compare to expectation
// expect(decision).toEqual(fixture.expectation);
});
it("does not mutate fixture inputs", () => {
for (const fixture of fixtures) {
const before = JSON.stringify(fixture);
runConformanceCase(fixture, REFERENCE_POLICY);
expect(JSON.stringify(fixture)).toBe(before);
}
});
});