Follows bitlogin's layering: the engine stays headless and the widget is a
consumer of the published API, never a dependency of it. Building it was also a
test of that API — an element needing something unexposed would have been a
finding, the way the commerce example was.
Viewer elements: governance-veil (blur on restrict, disclosure on hide, warning
on warn), governance-report, governance-status. Moderator elements:
governance-capabilities, governance-action, governance-admin-panel.
Deliberate behaviors:
- hidden content leaves the accessibility tree rather than being visually
covered, so a screen reader does not read out what the viewer must not see
- a hidden decision renders a disclosure, not nothing: content that silently
vanishes is indistinguishable from a bug and leaves no route to appeal
- refused moderator actions stay visible and name the missing capability. A
hidden button teaches nothing about the authority model, and the command
layer enforces the same check independently — the UI is an explanation,
never the gate
- no element computes policy; wording maps from stable reason identifiers and
is overridable per application
Found while building it: GovernanceAdminStore emitted only when reduced denial
state changed, so revoking a role that denied nobody announced nothing, leaving
capability-gated UI and the decision cache stale.
The root tsconfig deliberately omits the DOM lib — that absence is what keeps
core, nostr, and runtime from reaching for browser globals — so the widget
carries its own DOM-aware config rather than relaxing the guarantee globally.
687 tests; lint, typecheck, and the declaration build pass from a cold checkout.
Several contracts existed and were tested in isolation while nothing actually
used them. Reported as complete previously; this corrects that.
- storage was injected and never referenced. Adds persist/hydrate, hydration
before querying relays, and last-known-good state with a stale flag when a
relay is unreachable, which is what makes the characterized cache-fallback
behavior real rather than reduction-level only
- signature verification existed as a tested helper the load path never called.
Now applied through an injected verifier before state becomes authoritative,
with rejection counts and an enabled/disabled readout in diagnostics
- trusted mute lists had no subscription flow, so the store never populated in
a real deployment. Subscribes by author, chunked
- community source references decoded but were never resolved. Curator lists
are now fetched and merged with a source marker, still capability-checked
- root-published policy was announced and discarded. Now applied, accepted only
from the root, and a malformed document leaves the working policy intact
- adds import/export and capability queries
Two design corrections found while wiring:
- the cache key hashed the entire roster, so it rotated whenever a moderator
was added and the cache was discarded. Now derived from root identity alone.
- the root administrator is a constructor option: it is deployment config, and
hydration could not otherwise find its own cache, since the key derives from
the very fingerprint the cache would have supplied.
634 tests; lint, typecheck, and the declaration build clean.
The performance fixture the plan asks for found a real defect on its first run:
evaluation was quadratic in state size. Both the snapshot and its fingerprint
were rebuilt for every target, so each of 5,000 evaluations walked every report
and every mute list. A 5,000-target pass took ~47 seconds.
- the runtime memoizes the materialized snapshot and its fingerprint until a
store reports a change
- evaluateMany computes the fingerprint once and shares it across the batch
- decisions are cached per (profile, target); report and mute changes
invalidate only the targets they touch, while administrative, trust, policy,
and viewer changes drop the whole cache
- cached decisions are deeply frozen, since the cache hands the same object to
every caller
Same pass now takes ~85ms cold, ~6ms warm.
Adds tests/performance (5,000 targets, 500 authors, 100 muters per author) and
tests/security (one case per enumerated requirement testable at this layer).
605 tests; lint, typecheck, and the declaration build pass from a cold checkout.
Proves the engine did not stay video-specific during extraction.
- application adapter contract in core (createApplicationAdapter,
evaluateObject, collectTargets): an object names the targets that govern it
and the strictest verdict reaching any of them wins, so denying a seller
takes their whole catalogue down with no cascade logic in the application
- examples/commerce: a marketplace on the public API covering all ten
proof-of-fit behaviors the plan requires, including that no core change was
needed. One product, one snapshot, four surfaces, four different verdicts.
- README, integration guide, per-example README, CHANGELOG, four ADRs, and
GPL-3.0-or-later LICENSE (the license was listed in Phase 1 but missing)
- checkpoints rewritten to reflect actual state and to say plainly which
migration-plan phases belong to the consumer repos rather than here
Fixes a real gap found by the proof-of-fit: composeDecisions kept only the
first decision's evidence, so an object governed by several targets lost the
evidence that drove its verdict. Evidence now merges — counts summed,
contributor lists unioned, flags or-ed.
556 tests; lint, typecheck, and per-package type build clean.
Stores (Phase 4): admin, trust graph, reports, trusted mutes, policy, and
overrides. Each owns one slice of state and emits only when effective state
changes, so a consumer can treat every emission as a real invalidation.
- report storage is keyed target -> reporter -> category, making deduplication
structural: relays redeliver constantly and a redelivery must not inflate a count
- contribution lists replace by (actor, kind) and ignore out-of-order older
documents, so a late delivery cannot resurrect removed entries
- mute lists prune against an injected clock
- viewer switching clears blocks, mutes, and overrides but keeps operator seeds,
which are configuration rather than viewer data
Runtime: event routing, chunked report subscriptions (200 per filter, not one
per target), viewer lifecycle, evaluation, diagnostics, idempotent teardown.
Transport, signer, and storage are injected; in-memory doubles ship for tests.
Commands (Phase 5): every mutation checks capability locally before the signer
is invoked, so an unauthorized actor cannot produce a signed event at all.
Protected actors are refused at the command boundary as well as during
reduction. Partial relay acceptance succeeds while keeping failures visible.
Also fixes the type build: every package's "types" field pointed at a path the
build never produced, so consumers got no types at all. Declarations now emit
per package and resolve.
103 runtime tests; 505 total.
Event formats live in codecs so the core never learns which kind stores roles,
which d identifier is used, or how a legacy list is shaped.
- bech32/NIP-19 decoding, verified against the NIP-19 test vector, so the core
can stay hex-only and dependency-free
- canonical v1 codec on kind:30078 with namespaced d identifiers, role rosters
carrying both role labels and explicit capabilities, and size-bounded policy
documents that return null rather than throwing on malformed JSON
- NIP-56 reports, resolving the report type across the three tag layouts
clients use and excluding relay hints from the type position
- NIP-51 mute lists; encrypted entries are flagged, never decoded
- legacy admin-list codec supporting all five migration stages, with editors
surfaced separately so a UI permission cannot silently become signing authority
- replaceable selection with a deterministic same-timestamp tiebreak, without
which two relays returning events in different orders would produce different
state on different clients
129 codec tests; 402 total.
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.
Replace pnpm-only workspace: protocol with npm workspace deps, pin BitVid and
BitRoad reference commits, add Node types, and make the reference-import check
scan package graphs and source for forbidden bitvid/bitroad imports.