mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-10-27 08:31:00 +00:00
feat: adapter contract, commerce proof-of-fit, and reusability docs
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.
This commit is contained in:
1 parent
4d460c12fe
commit
2beb7a7bb5
24 files changed
+2732
-48
No files matched your search
@@ -0,0 +1,35 @@
|
||||
# ADR 0001 — Four independent decision dimensions
|
||||
|
||||
**Status:** accepted
|
||||
|
||||
## Context
|
||||
|
||||
The reference application collapsed moderation into a small number of booleans
|
||||
(`blurThumbnail`, `hideReason`, `blockAutoplay`), computed in two places that
|
||||
overlapped. A commerce consumer needs a state those booleans cannot express: a
|
||||
product that is visible, inspectable, downranked, and unbuyable at once.
|
||||
|
||||
## Decision
|
||||
|
||||
A decision carries four independent dimensions, each an ordered ladder:
|
||||
|
||||
- `ranking`: `normal` → `downrank` (with accumulated weight)
|
||||
- `visibility`: `allow` → `warn` → `restrict` → `hide` → `deny`
|
||||
- `interaction`: `allow` → `require-explicit-action` → `deny`
|
||||
- `transaction`: `allow` → `require-review` → `deny` (present only when a
|
||||
profile can affect it)
|
||||
|
||||
Composition takes the ladder maximum per dimension.
|
||||
|
||||
## Consequences
|
||||
|
||||
Composition is commutative and associative, so the order decisions arrive in
|
||||
cannot change the result — which is what lets an object be evaluated against
|
||||
several targets and merged without an ordering rule.
|
||||
|
||||
Consumers must map dimensions to their own UI rather than reading a boolean.
|
||||
That is more work at the call site and is the point: the mapping is application
|
||||
policy, and putting it in the engine is how the duplication started.
|
||||
|
||||
`transaction` is absent rather than `allow` when no profile gate can reach it,
|
||||
so a consumer can distinguish "explicitly permitted" from "not governed here".
|
||||
@@ -0,0 +1,34 @@
|
||||
# ADR 0002 — Capabilities resolve at merge time
|
||||
|
||||
**Status:** accepted
|
||||
|
||||
## Context
|
||||
|
||||
The reference application treated moderator status as a UI permission: an
|
||||
editor list gated which buttons appeared, but the resulting events carried no
|
||||
cryptographic authority. Anyone could publish the same event.
|
||||
|
||||
Revocation is the hard part. If a contribution's validity is decided when it is
|
||||
ingested, revoking a role later requires finding and undoing everything that
|
||||
actor ever contributed.
|
||||
|
||||
## Decision
|
||||
|
||||
Contributions are stored as-published, keyed by (actor, kind). Effective state
|
||||
is derived by `reduceAdminState`, which resolves each actor's capabilities
|
||||
against the *current* authority state every time it runs.
|
||||
|
||||
## Consequences
|
||||
|
||||
Revocation is immediate and requires no rewriting: drop the role, recompute,
|
||||
and that actor's entries are gone. A revoked moderator's stored contributions
|
||||
become inert rather than needing deletion.
|
||||
|
||||
Reduction cost is proportional to total contributions rather than to the change.
|
||||
For realistic list sizes this is far cheaper than maintaining incremental
|
||||
invalidation, and the store only recomputes when something actually changed.
|
||||
|
||||
An actor's contributions can also come *back* if their role is restored, which
|
||||
is the correct behavior for a mistaken revocation but must be understood when
|
||||
reasoning about "permanent" removal — permanence lives in the contribution
|
||||
being retracted, not in the role.
|
||||
@@ -0,0 +1,34 @@
|
||||
# ADR 0003 — The engine carries no thresholds
|
||||
|
||||
**Status:** accepted
|
||||
|
||||
## Context
|
||||
|
||||
The extraction started from one application's numbers: blur at 3 trusted
|
||||
reports, hide at 20 trusted mutes, a 60-day mute window. Those were nearly
|
||||
promoted into the core as defaults, which would have made every future consumer
|
||||
inherit a video application's editorial judgement.
|
||||
|
||||
## Decision
|
||||
|
||||
The core defines the *vocabulary* — target types, capabilities, effect ladders,
|
||||
reason identifiers, aggregation and precedence rules — and no thresholds.
|
||||
Applications supply a `PolicyDefinition` of named profiles with category
|
||||
thresholds. The characterized reference values live in
|
||||
`@nostr-governance/bitvid-compat` as one application's profile.
|
||||
|
||||
`NEUTRAL_POLICY` is the fallback: it enforces administrative denial and applies
|
||||
no trust thresholds, so the generic engine never hides content merely because
|
||||
one trusted account acted against it.
|
||||
|
||||
## Consequences
|
||||
|
||||
A new consumer writes a policy instead of arguing with a default. The commerce
|
||||
example needed no core change, which is the test this decision has to pass.
|
||||
|
||||
An application that supplies no policy gets very little enforcement. That is
|
||||
deliberate: silently applying someone else's moderation thresholds is worse than
|
||||
applying none, because the operator would not know it was happening.
|
||||
|
||||
Threshold `0` means "gate disabled" rather than "fires on everything", so
|
||||
zeroing a value is a safe way to switch a gate off.
|
||||
@@ -0,0 +1,26 @@
|
||||
# ADR 0004 — The core is hex-only
|
||||
|
||||
**Status:** accepted
|
||||
|
||||
## Context
|
||||
|
||||
Administrative lists in the wild contain both hex pubkeys and `npub` strings,
|
||||
and the reference implementation normalized them everywhere it touched them.
|
||||
Doing the same in the core would mean bech32 decoding in a package that is
|
||||
meant to have no dependencies and no encoding opinions.
|
||||
|
||||
## Decision
|
||||
|
||||
`@nostr-governance/core` accepts 64-character hex only. Decoding lives in
|
||||
`@nostr-governance/nostr` (`normalizePubkeyInput`, `normalizeEventIdInput`),
|
||||
and codecs normalize at ingest, so the core only ever sees hex.
|
||||
|
||||
## Consequences
|
||||
|
||||
The core stays dependency-free and runs unchanged in Node and the browser.
|
||||
|
||||
The trade-off is a real footgun: `normalizePubkey` returns `""` for a valid
|
||||
`npub` rather than throwing. Callers that hand user input straight to the core
|
||||
will silently get nothing. Anything accepting operator or user input must
|
||||
normalize through the codec package first — this is called out in the
|
||||
integration guide.
|
||||
+91
-45
@@ -1,63 +1,109 @@
|
||||
# Checkpoints
|
||||
|
||||
Scope note: this repository is the SDK. Migration-plan Phases 6–11 (BitVid
|
||||
shadow mode, cutovers, and duplicate removal) modify the consumer repositories
|
||||
and are tracked there, not here.
|
||||
|
||||
## Checkpoint 0 — Repo skeleton (completed)
|
||||
|
||||
Delivered:
|
||||
- npm workspaces monorepo
|
||||
- Node 22 + ESM
|
||||
- five empty packages under `packages/`
|
||||
- Vitest harness
|
||||
- TypeScript JSDoc typecheck
|
||||
- GitHub Actions CI
|
||||
- reference import guard
|
||||
- provenance docs
|
||||
Delivered: npm workspaces monorepo, Node 22 + ESM, Vitest, JSDoc typecheck,
|
||||
GitHub Actions CI, reference import guard, provenance docs.
|
||||
|
||||
Validation:
|
||||
```bash
|
||||
npm install
|
||||
npm run check:references
|
||||
npm run typecheck
|
||||
npm test
|
||||
npm run build:types
|
||||
Later additions: GPL-3.0-or-later `LICENSE`, `CHANGELOG.md`, architecture
|
||||
decision records under `docs/adr/`, and a working per-package type build. The
|
||||
`types` fields previously pointed at paths the build never produced, so
|
||||
consumers received no declarations; declarations now emit into each package.
|
||||
|
||||
## Checkpoint 1 — Characterization corpus (completed)
|
||||
|
||||
Delivered:
|
||||
- 34 fixtures under `fixtures/bitvid/cases/`, generated by
|
||||
`scripts/build-characterization-corpus.mjs`
|
||||
- conformance harness (`packages/testing/src/conformance.js`) that builds a
|
||||
snapshot, evaluates it, and diffs against a partial expectation
|
||||
- conformance suite (`tests/conformance/bitvid-fixtures.test.js`) asserting
|
||||
every required behavior, determinism, and input immutability
|
||||
|
||||
Characterized values, read from the pinned reference commit and cross-checked
|
||||
against the migration plan's baseline section:
|
||||
|
||||
```text
|
||||
Trusted report blur threshold: 3
|
||||
Trusted report autoplay threshold: 3
|
||||
Trusted mute hide threshold: 20
|
||||
Trusted spam-report hide threshold: 5
|
||||
Trusted mute validity window: 60 days
|
||||
```
|
||||
|
||||
Gate: do not add runtime or BitVid codecs until fixture corpus exists.
|
||||
These live in `@nostr-governance/bitvid-compat` as one application's profile,
|
||||
not as engine defaults.
|
||||
|
||||
## Checkpoint 1 — BitVid fixture and conformance corpus (completed)
|
||||
Provenance: 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.
|
||||
|
||||
Delivered:
|
||||
- fixture folders for BitVid cases (admin-state, reports, trusted-mutes, overrides, expectations)
|
||||
- import script harness (`scripts/import-bitvid-fixtures.mjs`)
|
||||
- characterization cases list (`fixtures/bitvid/characterization-cases.json`)
|
||||
- conformance test framework (`tests/conformance/bitvid-fixtures.test.js`)
|
||||
- testing utilities (`packages/testing/src/conformance.js`)
|
||||
## Checkpoint 2 — Pure core (completed)
|
||||
|
||||
Validation:
|
||||
```bash
|
||||
npm run check:references # still passes
|
||||
npm test # now includes conformance tests
|
||||
```
|
||||
Delivered: identifiers, targets, authority, administrative-state reduction,
|
||||
reasons, evidence, policy engine, fingerprints, evaluator, adapter contract.
|
||||
|
||||
Status: Ready for real fixture extraction from BitVid.
|
||||
Acceptance:
|
||||
- identical snapshots produce identical decisions (asserted per fixture)
|
||||
- evaluation performs no I/O and reads an injected clock
|
||||
- evaluation mutates no input (asserted per fixture)
|
||||
- the core imports no browser globals and has no dependencies
|
||||
- the core contains no application-specific terms or thresholds
|
||||
|
||||
Gate: Do not implement evaluator until at least 3 real characterization cases are imported with expectations.
|
||||
## Checkpoint 3 — Nostr codecs (completed)
|
||||
|
||||
Documentation: See `docs/phase1-summary.md` for full details.
|
||||
Delivered: bech32/NIP-19 (verified against the NIP-19 test vector), canonical v1
|
||||
on `kind:30078`, NIP-56 reports, NIP-51 mute lists, legacy administrative lists
|
||||
covering all five migration stages, replaceable selection with a deterministic
|
||||
same-timestamp tiebreak, injected signature verification.
|
||||
|
||||
## Checkpoint 2 — Pure core implementation (in progress)
|
||||
## Checkpoint 4 — Runtime stores (completed)
|
||||
|
||||
Delivered:
|
||||
- identifiers module (`packages/core/src/identifiers.js`)
|
||||
- targets module (`packages/core/src/targets.js`)
|
||||
- authority module (`packages/core/src/authority.js`)
|
||||
- policy module (`packages/core/src/policy.js`)
|
||||
- target creation, validation, and hierarchy utilities
|
||||
- role and capability management
|
||||
- policy definitions, decision making, and effect composition
|
||||
- comprehensive test suites for all modules
|
||||
Delivered: `GovernanceAdminStore`, `TrustGraphStore`, `ReportStore`,
|
||||
`TrustedMuteStore`, `PolicyStore`, `OverrideStore`, `GovernanceRuntime`.
|
||||
|
||||
Gate: Complete core implementation before moving to Nostr codecs.
|
||||
Acceptance: injected transport, storage, and clock; change-only emission; viewer
|
||||
switching clears viewer-scoped state while keeping operator seeds; subscriptions
|
||||
close on destroy; report subscriptions are chunked (200 per filter) rather than
|
||||
opened per target; author evidence is queryable in batches; expired mute lists
|
||||
prune against the injected clock.
|
||||
|
||||
## Next checkpoint
|
||||
## Checkpoint 5 — Public commands (completed)
|
||||
|
||||
Checkpoint 3 — Nostr codecs implementation (event parsing, legacy support)
|
||||
Delivered: contribution, role, policy, community-source, report, and override
|
||||
commands with stable error codes.
|
||||
|
||||
Acceptance: every command checks capability locally before the signer is
|
||||
invoked, so an unauthorized actor cannot produce a signed mutation; revoked
|
||||
actors' contributions stop affecting effective state; protected actors are
|
||||
refused at the command boundary; first relay acceptance completes the command
|
||||
while remaining relay results stay observable.
|
||||
|
||||
## Checkpoint 6 — Reusability proof (completed)
|
||||
|
||||
Delivered: `examples/commerce`, a marketplace built entirely on the public API,
|
||||
covering the ten proof-of-fit behaviors the migration plan requires — including
|
||||
that no core change was needed. Plus `README.md`, `docs/integration-guide.md`,
|
||||
and `examples/commerce/README.md`.
|
||||
|
||||
## Status
|
||||
|
||||
CI gates green: reference guard, typecheck, 556 tests, per-package type build.
|
||||
|
||||
## Not done here
|
||||
|
||||
Migration-plan Phases 6–11 operate on BitVid and BitRoad:
|
||||
|
||||
- Phase 6 — BitVid compatibility facade and shadow mode
|
||||
- Phase 7 — cut over BitVid administrative state
|
||||
- Phase 8 — cut over BitVid reports and trust
|
||||
- Phase 9 — cut over BitVid policy
|
||||
- Phase 10 — validate through BitRoad itself
|
||||
- Phase 11 — remove duplicated BitVid logic
|
||||
|
||||
The commerce example stands in for Phase 10's API-shape validation without
|
||||
touching the BitRoad repository.
|
||||
@@ -0,0 +1,237 @@
|
||||
# Integration guide
|
||||
|
||||
How to put this engine into an application that is not a video app.
|
||||
|
||||
Everything application-specific lives in two places you write: an **adapter**
|
||||
(what is my object, in governance terms) and a **policy** (what does the
|
||||
evidence mean to me). Nothing else needs to change, and nothing in the core
|
||||
needs to know your application exists.
|
||||
|
||||
## 1. Decide what your objects are
|
||||
|
||||
Three target types cover the model:
|
||||
|
||||
| Target | Use for |
|
||||
| --- | --- |
|
||||
| `user` | Accounts: authors, sellers, reviewers, reporters, curators |
|
||||
| `event` | One exact, immutable event that should stay denied on its own |
|
||||
| `address` | A replaceable coordinate: a product across revisions, a storefront, an article |
|
||||
|
||||
The distinction between `event` and `address` matters more than it looks.
|
||||
Denying an **address** survives the author republishing under a new event id.
|
||||
Denying an **event** does not — which is exactly what you want when only one
|
||||
revision was bad. Most objects want both.
|
||||
|
||||
## 2. Write an adapter
|
||||
|
||||
```js
|
||||
import { createApplicationAdapter } from "@nostr-governance/core";
|
||||
|
||||
export const productAdapter = createApplicationAdapter({
|
||||
applicationId: "my-shop",
|
||||
|
||||
// Every target whose status should reach this object.
|
||||
toTargets: (product) => [
|
||||
{ type: "user", pubkey: product.sellerPubkey },
|
||||
{ type: "event", id: product.id, author: product.sellerPubkey, kind: 30078 },
|
||||
{ type: "address", kind: "30078", pubkey: product.sellerPubkey, identifier: product.sku },
|
||||
],
|
||||
|
||||
// The object's own identity, used to key its decision.
|
||||
getPrimaryTargetKey: (product) => `address:30078:${product.sellerPubkey}:${product.sku}`,
|
||||
|
||||
// Optional: map the decision back into your shape.
|
||||
applyDecision: (product, decision) => ({
|
||||
...product,
|
||||
hidden: decision.visibility.effect === "hide",
|
||||
rankPenalty: decision.ranking.weight,
|
||||
checkoutAllowed: (decision.transaction?.effect ?? "allow") === "allow",
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
Composition across an object's targets is ladder-maximum per dimension: the
|
||||
strictest verdict reaching the object wins. That is what makes "deny the seller"
|
||||
take their whole catalogue down without you writing any cascade logic.
|
||||
|
||||
Be deliberate about what you *don't* include. A review should answer to its
|
||||
reviewer and its own event — not to the product it reviews, or a bad product
|
||||
would silence honest reviews of it.
|
||||
|
||||
## 3. Write a policy
|
||||
|
||||
A policy is a set of named **profiles**, one per surface. Profiles read the same
|
||||
evidence and are free to reach different conclusions.
|
||||
|
||||
```js
|
||||
import { createPolicyDefinition } from "@nostr-governance/core";
|
||||
|
||||
export const policy = createPolicyDefinition({
|
||||
id: "my-shop",
|
||||
version: "1.0.0",
|
||||
defaultProfile: "browse",
|
||||
profiles: {
|
||||
browse: {
|
||||
name: "browse",
|
||||
administrativeDeny: { visibility: "hide", interaction: "deny", transaction: "deny" },
|
||||
requireAllowlist: true,
|
||||
allowlistMiss: { visibility: "hide", interaction: "deny" },
|
||||
allowViewerOverride: false,
|
||||
reports: {
|
||||
malware: { hide: 1, transactionDeny: 1 },
|
||||
scam: { downrank: 1, restrict: 3, transactionDeny: 3 },
|
||||
default: { downrank: 2, warn: 4 },
|
||||
},
|
||||
mutes: { default: { downrank: 1 } },
|
||||
muteWindowSeconds: 60 * 24 * 60 * 60,
|
||||
},
|
||||
|
||||
checkout: {
|
||||
name: "checkout",
|
||||
administrativeDeny: { visibility: "allow", transaction: "deny" },
|
||||
allowViewerOverride: false,
|
||||
reports: { malware: { transactionDeny: 1 }, scam: { transactionDeny: 3 } },
|
||||
mutes: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Profile options
|
||||
|
||||
| Option | Effect |
|
||||
| --- | --- |
|
||||
| `administrativeDeny` | Effects applied when the target is administratively denied |
|
||||
| `viewerBlock` | Effects applied when the viewer personally blocks |
|
||||
| `requireAllowlist` / `allowlistMiss` | Enforce an allowlist on this surface |
|
||||
| `allowViewerOverride` | Whether a viewer may soften a decision |
|
||||
| `exposeEvidence` | Whether contributor pubkeys reach the consumer |
|
||||
| `bypassHide` / `bypassHideCeiling` | Cap a hide on discovery surfaces |
|
||||
| `disabled` | Turn governance off for this surface, with a reason |
|
||||
| `reports` / `mutes` | Category thresholds |
|
||||
| `muteWindowSeconds` | Ignore mutes older than this |
|
||||
|
||||
### Threshold gates
|
||||
|
||||
A gate fires when `count >= threshold`. A threshold of `0` means **disabled**,
|
||||
not "always on" — so you can zero a gate out without it triggering on every
|
||||
target.
|
||||
|
||||
```
|
||||
downrank → ranking warn, restrict, hide, deny → visibility
|
||||
requireExplicitAction, interactionDeny → interaction
|
||||
transactionReview, transactionDeny → transaction
|
||||
```
|
||||
|
||||
## 4. Supply a transport
|
||||
|
||||
The runtime depends on no relay pool. Implement three methods:
|
||||
|
||||
```js
|
||||
const transport = {
|
||||
async list(filters, options) { /* → NostrEvent[] */ },
|
||||
subscribe(filters, handlers, options) { /* → { close() } */ },
|
||||
async publish(event, options) {
|
||||
return { ok: true, accepted: ["wss://..."], failed: [] };
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
`publish` should report **partial** acceptance honestly: `ok: true` with a
|
||||
non-empty `failed` list is a normal, successful outcome. The first acceptance
|
||||
means the event exists on the network; the remaining results are diagnostics.
|
||||
|
||||
A signer (`getPublicKey`, `signEvent`) and storage (`read`, `write`, `remove`)
|
||||
follow the same injection pattern. In-memory versions ship for testing:
|
||||
|
||||
```js
|
||||
import { createMemoryTransport, createMemoryStorage } from "@nostr-governance/runtime";
|
||||
```
|
||||
|
||||
## 5. Wire the pipeline
|
||||
|
||||
```js
|
||||
import { collectTargets, evaluateObject } from "@nostr-governance/core";
|
||||
|
||||
runtime.setViewer(viewerPubkey); // do this FIRST — it clears viewer state
|
||||
runtime.trust.setContacts(followList); // then seed trust
|
||||
await runtime.loadAdministrativeState();
|
||||
|
||||
runtime.setActiveTargets(collectTargets(products, productAdapter));
|
||||
runtime.subscribeToActiveTargetReports();
|
||||
|
||||
const governed = products.map((product) =>
|
||||
productAdapter.applyDecision(
|
||||
product,
|
||||
evaluateObject(product, productAdapter, (target) =>
|
||||
runtime.evaluate(target, { profile: "browse" }),
|
||||
),
|
||||
),
|
||||
);
|
||||
```
|
||||
|
||||
Order matters in one place: `setViewer` clears viewer-scoped state by design, so
|
||||
seed contacts, blocks, and overrides *after* it, never before.
|
||||
|
||||
## 6. Define your roles
|
||||
|
||||
Roles are application-defined bundles over a fixed capability vocabulary:
|
||||
|
||||
```js
|
||||
import { createRoleDefinition } from "@nostr-governance/core";
|
||||
|
||||
const roles = {
|
||||
listing_moderator: createRoleDefinition("listing_moderator", [
|
||||
"contribute-event-deny",
|
||||
"contribute-address-deny",
|
||||
"review-evidence",
|
||||
]),
|
||||
seller_moderator: createRoleDefinition("seller_moderator", [
|
||||
"contribute-user-deny",
|
||||
"review-evidence",
|
||||
]),
|
||||
};
|
||||
```
|
||||
|
||||
If you pass a custom `roles` map to `setRoles`, it **replaces** the defaults —
|
||||
include `super_admin` yourself if you still want it. The root administrator
|
||||
always holds every capability regardless, so a misconfigured roster cannot lock
|
||||
you out of your own instance.
|
||||
|
||||
Every command checks capability locally before the signer is invoked. An
|
||||
unauthorized actor cannot produce a signed mutation at all, rather than
|
||||
publishing one that quietly fails to take effect.
|
||||
|
||||
## Common mistakes
|
||||
|
||||
**Recomputing policy in your view layer.** If your renderer is comparing report
|
||||
counts to thresholds, the extraction has failed. Consume `decision`.
|
||||
|
||||
**Treating `downrank` as a filter.** Downranking is a sort input. A downranked
|
||||
item still appears and can still be acted on; conflating the two is what makes
|
||||
moderation feel like censorship to the people affected.
|
||||
|
||||
**Skipping the checkout re-check.** Evaluate transaction-critical actions
|
||||
against their own profile at the moment of action. State moves between page
|
||||
render and button press.
|
||||
|
||||
**Exposing evidence everywhere.** `exposeEvidence` is off by default for a
|
||||
reason: public surfaces get counts, appeal surfaces get identities.
|
||||
|
||||
**Assuming an allowlist implies trust.** Being allowed to publish grants no
|
||||
trust, no moderator authority, and no immunity from personal blocks. The two
|
||||
systems are deliberately separate.
|
||||
|
||||
## Testing your integration
|
||||
|
||||
The conformance harness takes JSON fixtures and asserts decisions:
|
||||
|
||||
```js
|
||||
import { runConformanceCase } from "@nostr-governance/testing/conformance";
|
||||
|
||||
const { passed, mismatches } = runConformanceCase(fixture, policy);
|
||||
```
|
||||
|
||||
Expectations are partial — assert the dimensions your case is about and stay
|
||||
silent about the rest. See `fixtures/bitvid/cases/` for the shape and
|
||||
`examples/commerce/test/proof-of-fit.test.js` for a full application suite.
|
||||
Reference in new issue
Block a user