Add OpenABTest draft experiment and accounting contracts (#180)

This commit is contained in:
Anthony Ettinger 2026-09-13 02:17:35 -07:00 • committed by GitHub
parent 123ba2e39a
commit d8b3126782
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 1410 additions and 12 deletions

View file

@ -32,3 +32,13 @@ Use `@logicsrc/validators` 0.2.0 or newer for OpenRental validation. See the
bindings, exact decimal rental rates and CoinPay settlement metadata.
MIT © Profullstack, Inc.
## OpenABTest draft
Version 0.3.0 adds `OpenABTestManifest`, `OpenABTestEvent`,
`createOpenABTestManifest` and `createOpenABTestEvent`. The constructors set
`openabtest: "0.1-draft"`; validate their result with `@logicsrc/validators`
using `openabtest-manifest` or `openabtest-event` before storing it. They do not
assign participants, authenticate events or initiate payments. See the
[OpenABTest specification](https://logicsrc.com/docs/openabtest) and its complete
Chovy fixture for every referred purchase.

View file

@ -1,6 +1,6 @@
{
"name": "@logicsrc/sdk",
"version": "0.2.0",
"version": "0.3.0",
"description": "LogicSRC SDK contract types and client interface.",
"type": "module",
"main": "./dist/index.js",

View file

@ -94,3 +94,6 @@ export function createAgentSwarmSession(input: {
openspec_only: input.openspecOnly ?? false
};
}
export { createOpenABTestManifest, createOpenABTestEvent } from "./openabtest.js";
export type { OpenABTestManifest, OpenABTestEvent, OpenABTestPrice, OpenABTestReconciliation } from "./openabtest.js";

View file

@ -0,0 +1,81 @@
/** Draft private contracts; constructors do not assign users, authorize events or pay money. */
export interface OpenABTestManifest {
openabtest: "0.1-draft";
id: string;
revision: number;
name: string;
state: "draft" | "running" | "paused" | "closed";
cohort: { id: string; eligibilityRule: string; purchaseScope: "all-referred-purchases" | "all-eligible-purchases" | "first-eligible-purchase" | "custom" };
assignment: { unit: "user" | "account" | "session" | "device" | "custom"; authority: "server"; keyVersion: string; algorithm: string; persistence: "sticky" };
variants: Array<{ id: string; weight: number; parameters: Record<string, string | number | boolean | null> }>;
window: { startsAt: string; endsAt: string };
metrics: {
primary: "conversion-rate" | "retained-profit-per-eligible-visitor";
denominator: "unique-eligible-participants";
conversion: "unique-participants-with-confirmed-purchase";
profit: "reconciled-net-retained-profit";
minimumEligiblePerVariant: number;
minimumObservationSeconds: number;
winnerPolicy: "manual-review";
};
guardrails: { preserveAcceptedOffers: true; preserveAccruedCommissions: true; requireReconciledCostsForPayout: true; onBreach: "pause-new-assignments" };
economics?: { currency: string; minorUnitExponent: number; unit: string; listUnitPriceMinor: number; modeledUnitCostMinor: number | null; minimumRetainedUnitProfitMinor: number };
}
export interface OpenABTestPrice {
offerId: string;
acceptedAt: string;
currency: string;
listUnitPriceMinor: number;
chargedUnitPriceMinor: number;
discountBps: number;
quantityMilliUnits: number;
rounding: "half-up";
totalMinor: number;
}
type Assigned = { assignmentId: string; variantId: string };
type Converted = Assigned & { conversionId: string };
export interface OpenABTestReconciliation extends Converted {
accountingRevision: number;
currency: string;
state: "pending" | "reconciled";
revenueMinor: number;
refundsMinor: number;
actualCostMinor: number | null;
feesMinor: number | null;
affiliateMinor: number | null;
retainedProfitMinor: number | null;
minimumRetainedProfitMinor: number;
payoutStatus: "withheld" | "eligible";
proofRefs: string[];
}
type EventDetail =
| { kind: "eligibility"; payload: { cohortId: string; eligible: true } }
| { kind: "assignment"; payload: Assigned & { cohortId: string } }
| { kind: "exposure"; payload: Assigned & { offerId: string; surface: string } }
| { kind: "conversion"; payload: Converted & { price: OpenABTestPrice } }
| { kind: "adjustment"; payload: Converted & { adjustmentId: string; currency: string; amountMinor: number; reason: "refund" | "chargeback" | "correction"; proofRefs: string[] } }
| { kind: "reconciliation"; payload: OpenABTestReconciliation };
export type OpenABTestEvent = {
openabtest: "0.1-draft";
id: string;
producerId: string;
manifestId: string;
manifestRevision: number;
participantId: string;
at: string;
} & EventDetail;
/** Validate with @logicsrc/validators before storing or using the document. */
export function createOpenABTestManifest(input: Omit<OpenABTestManifest, "openabtest">): OpenABTestManifest {
return { ...input, openabtest: "0.1-draft" };
}
type EventInput = Omit<OpenABTestEvent, "openabtest" | "kind" | "payload"> & EventDetail;
/** The caller supplies authenticated context and durable IDs; this helper creates neither. */
export function createOpenABTestEvent(input: EventInput): OpenABTestEvent {
return { ...input, openabtest: "0.1-draft" };
}