mirror of
https://github.com/PR0M3TH3AN/bitgate.git
synced 2026-11-02 03:21:01 +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,156 @@
|
||||
// Commerce application adapters.
|
||||
//
|
||||
// Each adapter answers two questions: which governance targets stand behind
|
||||
// this object, and what does a decision mean for it. Note what is absent —
|
||||
// no thresholds, no precedence rules, no policy logic. Those live in the
|
||||
// policy definition and the engine respectively.
|
||||
|
||||
import { createApplicationAdapter } from "@nostr-governance/core";
|
||||
|
||||
/** Parameterized-replaceable kind used for commerce documents in this example. */
|
||||
export const PRODUCT_KIND = 30078;
|
||||
|
||||
/**
|
||||
* @typedef {Object} Product
|
||||
* @property {string} id - Event id of the current revision
|
||||
* @property {string} sellerPubkey
|
||||
* @property {string} identifier - d-tag, stable across revisions
|
||||
* @property {string} title
|
||||
* @property {number} priceSats
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Storefront
|
||||
* @property {string} id
|
||||
* @property {string} sellerPubkey
|
||||
* @property {string} identifier
|
||||
* @property {string} name
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} Review
|
||||
* @property {string} id
|
||||
* @property {string} reviewerPubkey
|
||||
* @property {string} productIdentifier
|
||||
* @property {number} rating
|
||||
*/
|
||||
|
||||
/**
|
||||
* Products answer to three targets.
|
||||
*
|
||||
* The address is what persists across revisions: denying it survives the seller
|
||||
* republishing the listing under a new event id, which denying the exact event
|
||||
* would not. Both are kept, because an exact-event denial must also stay
|
||||
* possible for the case where only one revision was bad.
|
||||
*
|
||||
* @type {import('@nostr-governance/core').GovernanceApplicationAdapter<Product>}
|
||||
*/
|
||||
export const productAdapter = createApplicationAdapter({
|
||||
applicationId: "commerce-example",
|
||||
|
||||
toTargets(product) {
|
||||
return [
|
||||
{ type: "user", pubkey: product.sellerPubkey },
|
||||
{ type: "event", id: product.id, author: product.sellerPubkey, kind: PRODUCT_KIND },
|
||||
{
|
||||
type: "address",
|
||||
kind: String(PRODUCT_KIND),
|
||||
pubkey: product.sellerPubkey,
|
||||
identifier: product.identifier,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
getPrimaryTargetKey(product) {
|
||||
return `address:${PRODUCT_KIND}:${product.sellerPubkey}:${product.identifier}`;
|
||||
},
|
||||
|
||||
applyDecision(product, decision) {
|
||||
return {
|
||||
...product,
|
||||
moderation: {
|
||||
visible: decision.visibility.effect !== "hide" && decision.visibility.effect !== "deny",
|
||||
warn: decision.visibility.effect === "warn",
|
||||
blurred: decision.visibility.effect === "restrict",
|
||||
downranked: decision.ranking.effect === "downrank",
|
||||
rankPenalty: decision.ranking.weight,
|
||||
checkoutAllowed: (decision.transaction?.effect ?? "allow") === "allow",
|
||||
checkoutNeedsReview: decision.transaction?.effect === "require-review",
|
||||
reasons: decision.reasons.map((reason) => reason.id),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Storefronts answer to the seller, the storefront address, and the exact event.
|
||||
* @type {import('@nostr-governance/core').GovernanceApplicationAdapter<Storefront>}
|
||||
*/
|
||||
export const storefrontAdapter = createApplicationAdapter({
|
||||
applicationId: "commerce-example",
|
||||
|
||||
toTargets(storefront) {
|
||||
return [
|
||||
{ type: "user", pubkey: storefront.sellerPubkey },
|
||||
{ type: "event", id: storefront.id, author: storefront.sellerPubkey, kind: PRODUCT_KIND },
|
||||
{
|
||||
type: "address",
|
||||
kind: String(PRODUCT_KIND),
|
||||
pubkey: storefront.sellerPubkey,
|
||||
identifier: storefront.identifier,
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
getPrimaryTargetKey(storefront) {
|
||||
return `address:${PRODUCT_KIND}:${storefront.sellerPubkey}:${storefront.identifier}`;
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Reviews answer to the reviewer and the exact review event.
|
||||
*
|
||||
* Deliberately not to the reviewed product: a bad product should not silence
|
||||
* honest reviews of it, which is exactly the outcome an over-broad target list
|
||||
* would produce.
|
||||
*
|
||||
* @type {import('@nostr-governance/core').GovernanceApplicationAdapter<Review>}
|
||||
*/
|
||||
export const reviewAdapter = createApplicationAdapter({
|
||||
applicationId: "commerce-example",
|
||||
|
||||
toTargets(review) {
|
||||
return [
|
||||
{ type: "user", pubkey: review.reviewerPubkey },
|
||||
{ type: "event", id: review.id, author: review.reviewerPubkey, kind: PRODUCT_KIND },
|
||||
];
|
||||
},
|
||||
|
||||
getPrimaryTargetKey(review) {
|
||||
return `event:${review.id}`;
|
||||
},
|
||||
|
||||
applyDecision(review, decision) {
|
||||
return {
|
||||
...review,
|
||||
suppressed: decision.visibility.effect === "hide" || decision.visibility.effect === "deny",
|
||||
downranked: decision.ranking.effect === "downrank",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Sellers answer only to their own user target.
|
||||
* @type {import('@nostr-governance/core').GovernanceApplicationAdapter<{ pubkey: string }>}
|
||||
*/
|
||||
export const sellerAdapter = createApplicationAdapter({
|
||||
applicationId: "commerce-example",
|
||||
|
||||
toTargets(seller) {
|
||||
return [{ type: "user", pubkey: seller.pubkey }];
|
||||
},
|
||||
|
||||
getPrimaryTargetKey(seller) {
|
||||
return `user:${seller.pubkey}`;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
// Marketplace pipeline.
|
||||
//
|
||||
// The application's job is reduced to: collect targets, ask the engine, apply
|
||||
// the answer. It computes no thresholds and reimplements no precedence — which
|
||||
// is the property the extraction is meant to guarantee.
|
||||
|
||||
import { collectTargets, evaluateObject } from "@nostr-governance/core";
|
||||
|
||||
import { productAdapter } from "./adapters.js";
|
||||
|
||||
/**
|
||||
* @typedef {import('./adapters.js').Product} Product
|
||||
*/
|
||||
|
||||
/**
|
||||
* Govern a product feed for a surface.
|
||||
*
|
||||
* Targets are collected first and handed to the runtime in one call, so a
|
||||
* hundred-item page opens a bounded number of subscriptions rather than one per
|
||||
* listing.
|
||||
*
|
||||
* @param {Product[]} products
|
||||
* @param {import('@nostr-governance/runtime').GovernanceRuntime} runtime
|
||||
* @param {string} profile
|
||||
* @returns {Array<Product & { moderation: any }>}
|
||||
*/
|
||||
export function governProductFeed(products, runtime, profile) {
|
||||
runtime.setActiveTargets(collectTargets(products, productAdapter));
|
||||
|
||||
return products.map((product) => {
|
||||
const decision = evaluateObject(product, productAdapter, (target) =>
|
||||
runtime.evaluate(target, { profile }),
|
||||
);
|
||||
return /** @type {any} */ (productAdapter.applyDecision?.(product, decision) ?? product);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a marketplace grid: drop hidden listings, order downranked ones last.
|
||||
*
|
||||
* Downranking is a sort input, not a filter. A downranked listing still appears
|
||||
* and can still be bought — conflating the two is what makes moderation feel
|
||||
* like censorship to sellers.
|
||||
*
|
||||
* @param {Product[]} products
|
||||
* @param {import('@nostr-governance/runtime').GovernanceRuntime} runtime
|
||||
* @returns {Array<Product & { moderation: any }>}
|
||||
*/
|
||||
export function buildMarketplaceGrid(products, runtime) {
|
||||
return governProductFeed(products, runtime, "public-marketplace")
|
||||
.filter((product) => product.moderation.visible)
|
||||
.sort((a, b) => a.moderation.rankPenalty - b.moderation.rankPenalty);
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} CheckoutVerdict
|
||||
* @property {boolean} allowed
|
||||
* @property {boolean} needsReview
|
||||
* @property {string[]} reasons
|
||||
*/
|
||||
|
||||
/**
|
||||
* Check a product at checkout.
|
||||
*
|
||||
* Evaluated freshly against the checkout profile rather than reusing the
|
||||
* listing page's decision: state may have changed since the page rendered, and
|
||||
* checkout is the point where being wrong actually costs someone money.
|
||||
*
|
||||
* @param {Product} product
|
||||
* @param {import('@nostr-governance/runtime').GovernanceRuntime} runtime
|
||||
* @returns {CheckoutVerdict}
|
||||
*/
|
||||
export function checkoutVerdict(product, runtime) {
|
||||
const decision = evaluateObject(product, productAdapter, (target) =>
|
||||
runtime.evaluate(target, { profile: "checkout" }),
|
||||
);
|
||||
|
||||
const effect = decision.transaction?.effect ?? "allow";
|
||||
return {
|
||||
allowed: effect === "allow",
|
||||
needsReview: effect === "require-review",
|
||||
reasons: decision.reasons.map((reason) => reason.id),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Explain a product's status to its seller.
|
||||
*
|
||||
* The seller-dashboard profile exposes evidence, so this is the one surface
|
||||
* that can answer "how many people reported me, and for what".
|
||||
*
|
||||
* @param {Product} product
|
||||
* @param {import('@nostr-governance/runtime').GovernanceRuntime} runtime
|
||||
* @returns {{ visible: boolean, sellable: boolean, reasons: string[], evidence: any }}
|
||||
*/
|
||||
export function explainToSeller(product, runtime) {
|
||||
const decision = evaluateObject(product, productAdapter, (target) =>
|
||||
runtime.evaluate(target, { profile: "seller-dashboard" }),
|
||||
);
|
||||
|
||||
return {
|
||||
visible: decision.visibility.effect !== "hide" && decision.visibility.effect !== "deny",
|
||||
sellable: (decision.transaction?.effect ?? "allow") === "allow",
|
||||
reasons: decision.reasons.map((reason) => reason.id),
|
||||
evidence: decision.evidence,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Commerce policy profiles.
|
||||
//
|
||||
// This example exists to prove the engine is not video-specific. Nothing here
|
||||
// touches @nostr-governance/core internals: it is built entirely from the
|
||||
// public API, and the core carries none of these numbers.
|
||||
//
|
||||
// The interesting property is that one product can simultaneously be visible to
|
||||
// its seller, downranked in discovery, inspectable on its detail page, and
|
||||
// blocked from checkout — four different answers from one decision, which a
|
||||
// single `hidden` boolean could not express.
|
||||
|
||||
import { createPolicyDefinition, createRoleDefinition } from "@nostr-governance/core";
|
||||
|
||||
/**
|
||||
* Category thresholds for a marketplace.
|
||||
*
|
||||
* Malware is treated far more harshly than misleading copy: one trusted report
|
||||
* blocks checkout, because the cost of a false negative is a compromised buyer,
|
||||
* while an over-eager `misleading` threshold would just suppress lawful listings.
|
||||
*
|
||||
* @type {Record<string, import('@nostr-governance/core').CategoryThresholds>}
|
||||
*/
|
||||
export const MARKETPLACE_REPORT_THRESHOLDS = {
|
||||
scam: {
|
||||
downrank: 1,
|
||||
warn: 2,
|
||||
restrict: 3,
|
||||
hide: 5,
|
||||
transactionReview: 2,
|
||||
transactionDeny: 3,
|
||||
},
|
||||
malware: {
|
||||
warn: 1,
|
||||
restrict: 1,
|
||||
hide: 2,
|
||||
transactionDeny: 1,
|
||||
},
|
||||
misleading: {
|
||||
downrank: 2,
|
||||
warn: 3,
|
||||
restrict: 5,
|
||||
hide: 8,
|
||||
},
|
||||
"not-as-described": {
|
||||
downrank: 2,
|
||||
warn: 4,
|
||||
transactionReview: 5,
|
||||
},
|
||||
default: {
|
||||
downrank: 2,
|
||||
warn: 4,
|
||||
},
|
||||
};
|
||||
|
||||
/** Trusted mutes downrank a seller but never block a sale on their own. */
|
||||
const MARKETPLACE_MUTE_THRESHOLDS = {
|
||||
default: { downrank: 1 },
|
||||
};
|
||||
|
||||
/**
|
||||
* The marketplace policy.
|
||||
*
|
||||
* Every surface reads the same evidence and reaches a different conclusion,
|
||||
* which is the whole point of profiles.
|
||||
*/
|
||||
export const COMMERCE_POLICY = createPolicyDefinition({
|
||||
id: "commerce-example",
|
||||
name: "Commerce example",
|
||||
description: "Marketplace policy demonstrating the engine outside a video application.",
|
||||
version: "1.0.0",
|
||||
defaultProfile: "public-marketplace",
|
||||
profiles: {
|
||||
// Discovery: denied sellers disappear entirely and cannot transact.
|
||||
"public-marketplace": {
|
||||
name: "public-marketplace",
|
||||
administrativeDeny: { visibility: "hide", interaction: "deny", transaction: "deny" },
|
||||
viewerBlock: { visibility: "hide", interaction: "deny" },
|
||||
allowViewerOverride: false,
|
||||
requireAllowlist: true,
|
||||
allowlistMiss: { visibility: "hide", interaction: "deny", transaction: "deny" },
|
||||
reports: MARKETPLACE_REPORT_THRESHOLDS,
|
||||
mutes: MARKETPLACE_MUTE_THRESHOLDS,
|
||||
},
|
||||
|
||||
// Detail page: a shopper who followed a direct link may still inspect the
|
||||
// listing behind a warning, but cannot buy it.
|
||||
"product-detail": {
|
||||
name: "product-detail",
|
||||
administrativeDeny: { visibility: "restrict", interaction: "require-explicit-action", transaction: "deny" },
|
||||
viewerBlock: { visibility: "hide", interaction: "deny" },
|
||||
allowViewerOverride: true,
|
||||
reports: MARKETPLACE_REPORT_THRESHOLDS,
|
||||
mutes: MARKETPLACE_MUTE_THRESHOLDS,
|
||||
},
|
||||
|
||||
// Checkout: visibility is irrelevant; only the transaction verdict matters,
|
||||
// and it is re-checked here rather than trusted from the listing page.
|
||||
checkout: {
|
||||
name: "checkout",
|
||||
administrativeDeny: { visibility: "allow", transaction: "deny" },
|
||||
allowViewerOverride: false,
|
||||
reports: MARKETPLACE_REPORT_THRESHOLDS,
|
||||
mutes: {},
|
||||
},
|
||||
|
||||
// Seller dashboard: the seller can always see their own listing and is told
|
||||
// why it is restricted. An appeals screen that hides the evidence is useless.
|
||||
"seller-dashboard": {
|
||||
name: "seller-dashboard",
|
||||
administrativeDeny: { visibility: "warn", interaction: "allow", transaction: "deny" },
|
||||
allowViewerOverride: false,
|
||||
exposeEvidence: true,
|
||||
reports: MARKETPLACE_REPORT_THRESHOLDS,
|
||||
mutes: MARKETPLACE_MUTE_THRESHOLDS,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Marketplace-specific roles, built from the generic capability vocabulary.
|
||||
*
|
||||
* These roles do not exist in the core; an application invents the roles it
|
||||
* needs and the engine enforces them.
|
||||
*/
|
||||
export const COMMERCE_ROLES = {
|
||||
listing_moderator: createRoleDefinition("listing_moderator", [
|
||||
"contribute-event-deny",
|
||||
"contribute-address-deny",
|
||||
"review-evidence",
|
||||
]),
|
||||
seller_moderator: createRoleDefinition("seller_moderator", [
|
||||
"contribute-user-deny",
|
||||
"review-evidence",
|
||||
]),
|
||||
community_curator: createRoleDefinition("community_curator", ["contribute-user-deny"]),
|
||||
};
|
||||
|
||||
/**
|
||||
* Role capability map in the shape createAuthorityState expects.
|
||||
* @type {Record<string, import('@nostr-governance/core').GovernanceCapability[]>}
|
||||
*/
|
||||
export const COMMERCE_ROLE_CAPABILITIES = Object.fromEntries(
|
||||
Object.values(COMMERCE_ROLES).map((role) => [role.name, role.capabilities]),
|
||||
);
|
||||
Reference in new issue
Block a user