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

@ -0,0 +1,22 @@
{
"openabtest": "0.1-draft",
"producerId": "chovy-server",
"manifestId": "chovy-signup-discount-v1",
"manifestRevision": 1,
"participantId": "participant-opaque-7d12",
"at": "2026-09-15T12:00:00Z",
"id": "event-adjustment-42",
"kind": "adjustment",
"payload": {
"assignmentId": "assignment-42",
"variantId": "signup-10",
"conversionId": "purchase-42",
"adjustmentId": "refund-42",
"currency": "USD",
"amountMinor": -3600,
"reason": "refund",
"proofRefs": [
"refund-proof-42"
]
}
}

View file

@ -0,0 +1,15 @@
{
"openabtest": "0.1-draft",
"producerId": "chovy-server",
"manifestId": "chovy-signup-discount-v1",
"manifestRevision": 1,
"participantId": "participant-opaque-7d12",
"at": "2026-09-15T12:00:00Z",
"id": "event-assignment-42",
"kind": "assignment",
"payload": {
"assignmentId": "assignment-42",
"variantId": "signup-10",
"cohortId": "eligible-referred-customers"
}
}

View file

@ -0,0 +1,69 @@
{
"openabtest": "0.1-draft",
"id": "chovy-signup-discount-v1",
"revision": 1,
"name": "Chovy referred purchase discount",
"state": "draft",
"cohort": {
"id": "eligible-referred-customers",
"eligibilityRule": "Server-verified eligible referral; exclude self-referrals and invalid attribution. Every referred purchase qualifies.",
"purchaseScope": "all-referred-purchases"
},
"assignment": {
"unit": "user",
"authority": "server",
"keyVersion": "1",
"algorithm": "hmac-sha256-persisted-v1",
"persistence": "sticky"
},
"variants": [
{
"id": "signup-5",
"weight": 1,
"parameters": {
"discountBps": 500
}
},
{
"id": "signup-10",
"weight": 1,
"parameters": {
"discountBps": 1000
}
},
{
"id": "signup-20",
"weight": 1,
"parameters": {
"discountBps": 2000
}
}
],
"window": {
"startsAt": "2026-09-14T00:00:00Z",
"endsAt": "2026-10-14T00:00:00Z"
},
"metrics": {
"primary": "retained-profit-per-eligible-visitor",
"denominator": "unique-eligible-participants",
"conversion": "unique-participants-with-confirmed-purchase",
"profit": "reconciled-net-retained-profit",
"minimumEligiblePerVariant": 100,
"minimumObservationSeconds": 604800,
"winnerPolicy": "manual-review"
},
"guardrails": {
"preserveAcceptedOffers": true,
"preserveAccruedCommissions": true,
"requireReconciledCostsForPayout": true,
"onBreach": "pause-new-assignments"
},
"economics": {
"currency": "USD",
"minorUnitExponent": 2,
"unit": "agent-hour",
"listUnitPriceMinor": 40000,
"modeledUnitCostMinor": 10000,
"minimumRetainedUnitProfitMinor": 5000
}
}

View file

@ -0,0 +1,26 @@
{
"openabtest": "0.1-draft",
"producerId": "chovy-server",
"manifestId": "chovy-signup-discount-v1",
"manifestRevision": 1,
"participantId": "participant-opaque-7d12",
"at": "2026-09-15T12:00:00Z",
"id": "event-conversion-42",
"kind": "conversion",
"payload": {
"assignmentId": "assignment-42",
"variantId": "signup-10",
"conversionId": "purchase-42",
"price": {
"offerId": "offer-42",
"acceptedAt": "2026-09-15T11:59:00Z",
"currency": "USD",
"listUnitPriceMinor": 40000,
"chargedUnitPriceMinor": 36000,
"discountBps": 1000,
"quantityMilliUnits": 1000,
"rounding": "half-up",
"totalMinor": 36000
}
}
}

View file

@ -0,0 +1,14 @@
{
"openabtest": "0.1-draft",
"producerId": "chovy-server",
"manifestId": "chovy-signup-discount-v1",
"manifestRevision": 1,
"participantId": "participant-opaque-7d12",
"at": "2026-09-15T12:00:00Z",
"id": "event-eligibility-42",
"kind": "eligibility",
"payload": {
"cohortId": "eligible-referred-customers",
"eligible": true
}
}

View file

@ -0,0 +1,16 @@
{
"openabtest": "0.1-draft",
"producerId": "chovy-server",
"manifestId": "chovy-signup-discount-v1",
"manifestRevision": 1,
"participantId": "participant-opaque-7d12",
"at": "2026-09-15T12:00:00Z",
"id": "event-exposure-42",
"kind": "exposure",
"payload": {
"assignmentId": "assignment-42",
"variantId": "signup-10",
"offerId": "offer-42",
"surface": "checkout"
}
}

View file

@ -0,0 +1,27 @@
{
"openabtest": "0.1-draft",
"producerId": "chovy-server",
"manifestId": "chovy-signup-discount-v1",
"manifestRevision": 1,
"participantId": "participant-opaque-7d12",
"at": "2026-09-15T12:00:00Z",
"id": "event-reconciliation-pending-42",
"kind": "reconciliation",
"payload": {
"assignmentId": "assignment-42",
"variantId": "signup-10",
"conversionId": "purchase-42",
"accountingRevision": 1,
"currency": "USD",
"state": "pending",
"revenueMinor": 36000,
"refundsMinor": 0,
"actualCostMinor": null,
"feesMinor": null,
"affiliateMinor": null,
"retainedProfitMinor": null,
"minimumRetainedProfitMinor": 5000,
"payoutStatus": "withheld",
"proofRefs": []
}
}

View file

@ -0,0 +1,30 @@
{
"openabtest": "0.1-draft",
"producerId": "chovy-server",
"manifestId": "chovy-signup-discount-v1",
"manifestRevision": 1,
"participantId": "participant-opaque-7d12",
"at": "2026-09-15T12:00:00Z",
"id": "event-reconciliation-42",
"kind": "reconciliation",
"payload": {
"assignmentId": "assignment-42",
"variantId": "signup-10",
"conversionId": "purchase-42",
"accountingRevision": 2,
"currency": "USD",
"state": "reconciled",
"revenueMinor": 36000,
"refundsMinor": 0,
"actualCostMinor": 10000,
"feesMinor": 1000,
"affiliateMinor": 20000,
"retainedProfitMinor": 5000,
"minimumRetainedProfitMinor": 5000,
"payoutStatus": "eligible",
"proofRefs": [
"cost-proof-42",
"fees-proof-42"
]
}
}

View file

@ -1,7 +1,7 @@
{
"name": "@logicsrc/schemas",
"version": "0.2.0",
"description": "LogicSRC JSON schemas for tasks, agents, runs, events, plugins, the AgentAd ad standard, the OpenOntology knowledge contracts, the OpenContext context plane, the OpenCreds credential vault, OpenRental membership and rental offers, and the OpenWall messaging proposal.",
"version": "0.3.0",
"description": "LogicSRC JSON schemas for tasks, agents, runs, events, plugins, the AgentAd ad standard, the OpenOntology knowledge contracts, the OpenContext context plane, the OpenCreds credential vault, OpenRental membership and rental offers, and the OpenWall messaging proposal. Includes the OpenABTest draft experiment and event contracts.",
"license": "MIT",
"type": "module",
"repository": {
@ -26,7 +26,8 @@
"openontology",
"password-manager",
"standards",
"vault"
"vault",
"openabtest"
],
"publishConfig": {
"access": "public"
@ -92,7 +93,9 @@
"./repo": "./schemas/logicsrc-repo.schema.json",
"./run": "./schemas/logicsrc-run.schema.json",
"./social-post": "./schemas/logicsrc-social-post.schema.json",
"./task": "./schemas/logicsrc-task.schema.json"
"./task": "./schemas/logicsrc-task.schema.json",
"./openabtest-manifest": "./schemas/logicsrc-openabtest-manifest.schema.json",
"./openabtest-event": "./schemas/logicsrc-openabtest-event.schema.json"
},
"files": [
"schemas",

View file

@ -0,0 +1,460 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://logicsrc.com/schemas/logicsrc-openabtest-event.schema.json",
"title": "OpenABTest authorized event (0.1 draft)",
"type": "object",
"additionalProperties": false,
"required": [
"openabtest",
"id",
"producerId",
"manifestId",
"manifestRevision",
"participantId",
"at",
"kind",
"payload"
],
"properties": {
"openabtest": {
"const": "0.1-draft"
},
"id": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"producerId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"manifestId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"manifestRevision": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"participantId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"at": {
"type": "string",
"format": "date-time"
},
"kind": {
"enum": [
"eligibility",
"assignment",
"exposure",
"conversion",
"adjustment",
"reconciliation"
]
},
"payload": {
"type": "object"
}
},
"allOf": [
{
"if": {
"properties": {
"kind": {
"const": "eligibility"
}
}
},
"then": {
"properties": {
"payload": {
"type": "object",
"additionalProperties": false,
"required": [
"cohortId",
"eligible"
],
"properties": {
"cohortId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"eligible": {
"const": true
}
}
}
}
}
},
{
"if": {
"properties": {
"kind": {
"const": "assignment"
}
}
},
"then": {
"properties": {
"payload": {
"type": "object",
"additionalProperties": false,
"required": [
"assignmentId",
"variantId",
"cohortId"
],
"properties": {
"assignmentId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"variantId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"cohortId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
}
}
}
}
}
},
{
"if": {
"properties": {
"kind": {
"const": "exposure"
}
}
},
"then": {
"properties": {
"payload": {
"type": "object",
"additionalProperties": false,
"required": [
"assignmentId",
"variantId",
"offerId",
"surface"
],
"properties": {
"assignmentId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"variantId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"offerId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"surface": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
}
}
}
}
}
},
{
"if": {
"properties": {
"kind": {
"const": "conversion"
}
}
},
"then": {
"properties": {
"payload": {
"type": "object",
"additionalProperties": false,
"required": [
"assignmentId",
"variantId",
"conversionId",
"price"
],
"properties": {
"assignmentId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"variantId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"conversionId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"price": {
"type": "object",
"additionalProperties": false,
"required": [
"offerId",
"acceptedAt",
"currency",
"listUnitPriceMinor",
"chargedUnitPriceMinor",
"discountBps",
"quantityMilliUnits",
"rounding",
"totalMinor"
],
"properties": {
"offerId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"acceptedAt": {
"type": "string",
"format": "date-time"
},
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$"
},
"listUnitPriceMinor": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"chargedUnitPriceMinor": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"discountBps": {
"type": "integer",
"minimum": 0,
"maximum": 10000
},
"quantityMilliUnits": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"rounding": {
"const": "half-up"
},
"totalMinor": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
}
}
}
}
}
}
},
{
"if": {
"properties": {
"kind": {
"const": "adjustment"
}
}
},
"then": {
"properties": {
"payload": {
"type": "object",
"additionalProperties": false,
"required": [
"assignmentId",
"variantId",
"conversionId",
"adjustmentId",
"currency",
"amountMinor",
"reason",
"proofRefs"
],
"properties": {
"assignmentId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"variantId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"conversionId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"adjustmentId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$"
},
"amountMinor": {
"type": "integer",
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"reason": {
"enum": [
"refund",
"chargeback",
"correction"
]
},
"proofRefs": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"uniqueItems": true,
"maxItems": 100,
"minItems": 1
}
}
}
}
}
},
{
"if": {
"properties": {
"kind": {
"const": "reconciliation"
}
}
},
"then": {
"properties": {
"payload": {
"type": "object",
"additionalProperties": false,
"required": [
"assignmentId",
"variantId",
"conversionId",
"accountingRevision",
"currency",
"state",
"revenueMinor",
"refundsMinor",
"actualCostMinor",
"feesMinor",
"affiliateMinor",
"retainedProfitMinor",
"minimumRetainedProfitMinor",
"payoutStatus",
"proofRefs"
],
"properties": {
"assignmentId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"variantId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"conversionId": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"accountingRevision": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$"
},
"state": {
"enum": [
"pending",
"reconciled"
]
},
"revenueMinor": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"refundsMinor": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"actualCostMinor": {
"type": [
"integer",
"null"
],
"minimum": 0,
"maximum": 9007199254740991
},
"feesMinor": {
"type": [
"integer",
"null"
],
"minimum": 0,
"maximum": 9007199254740991
},
"affiliateMinor": {
"type": [
"integer",
"null"
],
"minimum": 0,
"maximum": 9007199254740991
},
"retainedProfitMinor": {
"type": [
"integer",
"null"
],
"minimum": -9007199254740991,
"maximum": 9007199254740991
},
"minimumRetainedProfitMinor": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"payoutStatus": {
"enum": [
"withheld",
"eligible"
]
},
"proofRefs": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"uniqueItems": true,
"maxItems": 100
}
}
}
}
}
}
]
}

View file

@ -0,0 +1,291 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://logicsrc.com/schemas/logicsrc-openabtest-manifest.schema.json",
"title": "OpenABTest experiment manifest (0.1 draft)",
"type": "object",
"additionalProperties": false,
"required": [
"openabtest",
"id",
"revision",
"name",
"state",
"cohort",
"assignment",
"variants",
"window",
"metrics",
"guardrails"
],
"properties": {
"openabtest": {
"const": "0.1-draft"
},
"id": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"revision": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 1000
},
"state": {
"enum": [
"draft",
"running",
"paused",
"closed"
]
},
"cohort": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"eligibilityRule",
"purchaseScope"
],
"properties": {
"id": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"eligibilityRule": {
"type": "string",
"minLength": 1,
"maxLength": 1000
},
"purchaseScope": {
"enum": [
"all-referred-purchases",
"all-eligible-purchases",
"first-eligible-purchase",
"custom"
]
}
}
},
"assignment": {
"type": "object",
"additionalProperties": false,
"required": [
"unit",
"authority",
"keyVersion",
"algorithm",
"persistence"
],
"properties": {
"unit": {
"enum": [
"user",
"account",
"session",
"device",
"custom"
]
},
"authority": {
"const": "server"
},
"keyVersion": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"algorithm": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"persistence": {
"const": "sticky"
}
}
},
"variants": {
"type": "array",
"minItems": 2,
"maxItems": 100,
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"id",
"weight",
"parameters"
],
"properties": {
"id": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"weight": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"parameters": {
"type": "object",
"minProperties": 1,
"maxProperties": 50,
"propertyNames": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"additionalProperties": {
"anyOf": [
{
"type": "string"
},
{
"type": "number"
},
{
"type": "boolean"
},
{
"type": "null"
}
]
}
}
}
}
},
"window": {
"type": "object",
"additionalProperties": false,
"required": [
"startsAt",
"endsAt"
],
"properties": {
"startsAt": {
"type": "string",
"format": "date-time"
},
"endsAt": {
"type": "string",
"format": "date-time"
}
}
},
"metrics": {
"type": "object",
"additionalProperties": false,
"required": [
"primary",
"denominator",
"conversion",
"profit",
"minimumEligiblePerVariant",
"minimumObservationSeconds",
"winnerPolicy"
],
"properties": {
"primary": {
"enum": [
"conversion-rate",
"retained-profit-per-eligible-visitor"
]
},
"denominator": {
"const": "unique-eligible-participants"
},
"conversion": {
"const": "unique-participants-with-confirmed-purchase"
},
"profit": {
"const": "reconciled-net-retained-profit"
},
"minimumEligiblePerVariant": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"minimumObservationSeconds": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991
},
"winnerPolicy": {
"const": "manual-review"
}
}
},
"guardrails": {
"type": "object",
"additionalProperties": false,
"required": [
"preserveAcceptedOffers",
"preserveAccruedCommissions",
"requireReconciledCostsForPayout",
"onBreach"
],
"properties": {
"preserveAcceptedOffers": {
"const": true
},
"preserveAccruedCommissions": {
"const": true
},
"requireReconciledCostsForPayout": {
"const": true
},
"onBreach": {
"const": "pause-new-assignments"
}
}
},
"economics": {
"type": "object",
"additionalProperties": false,
"required": [
"currency",
"minorUnitExponent",
"unit",
"listUnitPriceMinor",
"modeledUnitCostMinor",
"minimumRetainedUnitProfitMinor"
],
"properties": {
"currency": {
"type": "string",
"pattern": "^[A-Z]{3}$"
},
"minorUnitExponent": {
"type": "integer",
"minimum": 0,
"maximum": 6
},
"unit": {
"type": "string",
"pattern": "^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$"
},
"listUnitPriceMinor": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
},
"modeledUnitCostMinor": {
"type": [
"integer",
"null"
],
"minimum": 0,
"maximum": 9007199254740991
},
"minimumRetainedUnitProfitMinor": {
"type": "integer",
"minimum": 0,
"maximum": 9007199254740991
}
}
}
}
}

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" };
}

View file

@ -1,6 +1,6 @@
{
"name": "@logicsrc/validators",
"version": "0.2.0",
"version": "0.3.0",
"description": "LogicSRC schema validation helpers.",
"type": "module",
"main": "./dist/index.js",
@ -11,10 +11,10 @@
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "vitest run src",
"validate:fixtures": "node dist/cli.js task ../schemas/fixtures/task.yaml && node dist/cli.js agent ../schemas/fixtures/agent.yaml && node dist/cli.js agentad-ad ../schemas/fixtures/agentad-ad.yaml && node dist/cli.js agentad-placement ../schemas/fixtures/agentad-placement.yaml && node dist/cli.js repo ../schemas/fixtures/repo.yaml && node dist/cli.js pull-request ../schemas/fixtures/pull-request.yaml && node dist/cli.js openontology-manifest ../schemas/fixtures/openontology/valid/manifest.json && node dist/cli.js openontology-claim ../schemas/fixtures/openontology/valid/claim-relationship.json && node dist/cli.js openontology-changeset ../schemas/fixtures/openontology/valid/changeset.json && node dist/cli.js opencontext-manifest ../schemas/fixtures/opencontext/valid/manifest.json && node dist/cli.js opencontext-object ../schemas/fixtures/opencontext/valid/object-policy.json && node dist/cli.js opencontext-bundle ../schemas/fixtures/opencontext/valid/bundle.json && node dist/cli.js opencontext-decision ../schemas/fixtures/opencontext/valid/decision.json && node dist/cli.js opencontext-role ../schemas/fixtures/opencontext/valid/role.json && node dist/cli.js opencontext-provenance ../schemas/fixtures/opencontext/valid/provenance.json && node dist/cli.js opencontext-diagnostic ../schemas/fixtures/opencontext/valid/diagnostic.json && node dist/cli.js opencontext-audit-event ../schemas/fixtures/opencontext/valid/audit-event.json && node dist/cli.js openrental ../schemas/fixtures/openrental/mixed.json && node dist/cli.js openwall-message ../schemas/fixtures/openwall/broadcast.json && node dist/cli.js openwall-message ../schemas/fixtures/openwall/direct.json && node dist/cli.js openwall-message ../schemas/fixtures/openwall/announcement.json && node dist/cli.js openwall-receipt ../schemas/fixtures/openwall/receipt-accepted.json && node dist/cli.js openwall-receipt ../schemas/fixtures/openwall/receipt-retrying.json"
"validate:fixtures": "node dist/cli.js task ../schemas/fixtures/task.yaml && node dist/cli.js agent ../schemas/fixtures/agent.yaml && node dist/cli.js agentad-ad ../schemas/fixtures/agentad-ad.yaml && node dist/cli.js agentad-placement ../schemas/fixtures/agentad-placement.yaml && node dist/cli.js repo ../schemas/fixtures/repo.yaml && node dist/cli.js pull-request ../schemas/fixtures/pull-request.yaml && node dist/cli.js openontology-manifest ../schemas/fixtures/openontology/valid/manifest.json && node dist/cli.js openontology-claim ../schemas/fixtures/openontology/valid/claim-relationship.json && node dist/cli.js openontology-changeset ../schemas/fixtures/openontology/valid/changeset.json && node dist/cli.js opencontext-manifest ../schemas/fixtures/opencontext/valid/manifest.json && node dist/cli.js opencontext-object ../schemas/fixtures/opencontext/valid/object-policy.json && node dist/cli.js opencontext-bundle ../schemas/fixtures/opencontext/valid/bundle.json && node dist/cli.js opencontext-decision ../schemas/fixtures/opencontext/valid/decision.json && node dist/cli.js opencontext-role ../schemas/fixtures/opencontext/valid/role.json && node dist/cli.js opencontext-provenance ../schemas/fixtures/opencontext/valid/provenance.json && node dist/cli.js opencontext-diagnostic ../schemas/fixtures/opencontext/valid/diagnostic.json && node dist/cli.js opencontext-audit-event ../schemas/fixtures/opencontext/valid/audit-event.json && node dist/cli.js openrental ../schemas/fixtures/openrental/mixed.json && node dist/cli.js openwall-message ../schemas/fixtures/openwall/broadcast.json && node dist/cli.js openwall-message ../schemas/fixtures/openwall/direct.json && node dist/cli.js openwall-message ../schemas/fixtures/openwall/announcement.json && node dist/cli.js openwall-receipt ../schemas/fixtures/openwall/receipt-accepted.json && node dist/cli.js openwall-receipt ../schemas/fixtures/openwall/receipt-retrying.json && node dist/cli.js openabtest-manifest ../schemas/fixtures/openabtest/chovy-manifest.json && node dist/cli.js openabtest-event ../schemas/fixtures/openabtest/reconciliation.json"
},
"dependencies": {
"@logicsrc/schemas": "^0.2.0",
"@logicsrc/schemas": "^0.3.0",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1",
"yaml": "^2.8.1"

View file

@ -3,6 +3,7 @@ import * as addFormatsModule from "ajv-formats";
import type { ErrorObject } from "ajv";
import { parse } from "yaml";
import { isSchemaKind, schemas, type SchemaKind } from "./schemas.js";
import { validateOpenABTest } from "./openabtest.js";
import { validateOpenRentalReferences } from "./openrental.js";
type CompiledSchema = { (data: unknown): boolean; errors?: ErrorObject[] | null };
@ -66,6 +67,10 @@ export function validate(kind: SchemaKind, data: unknown): ValidationResult {
const errors = validateOpenRentalReferences(data);
if (errors.length) return { ok: false, kind, errors };
}
if (kind === "openabtest-manifest" || kind === "openabtest-event") {
const errors = validateOpenABTest(kind, data);
if (errors.length) return { ok: false, kind, errors };
}
return { ok: true, kind, data };
}

View file

@ -0,0 +1,103 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
import { validate } from "./index.js";
function fixture(name: string) {
return JSON.parse(readFileSync(new URL(`../../schemas/fixtures/openabtest/${name}.json`, import.meta.url), "utf8"));
}
describe("OpenABTest draft contracts", () => {
it("accepts the every-referred-purchase experiment", () => {
expect(validate("openabtest-manifest", fixture("chovy-manifest")).ok).toBe(true);
});
it.each(["eligibility", "assignment", "exposure", "conversion", "adjustment", "reconciliation-pending", "reconciliation"])("accepts %s independently", (name) => {
expect(validate("openabtest-event", fixture(name)).ok).toBe(true);
});
it("rejects duplicate variant identities even when weights differ", () => {
const manifest = fixture("chovy-manifest");
manifest.variants[1].id = manifest.variants[0].id;
manifest.variants[1].weight = 2;
expect(validate("openabtest-manifest", manifest).ok).toBe(false);
});
it.each([0, -1, 0.5, Number.MAX_SAFE_INTEGER + 1])("rejects invalid weight %s", (weight) => {
const manifest = fixture("chovy-manifest");
manifest.variants[0].weight = weight;
expect(validate("openabtest-manifest", manifest).ok).toBe(false);
});
it("rejects unsafe sums and backwards windows", () => {
const manifest = fixture("chovy-manifest");
manifest.variants[0].weight = Number.MAX_SAFE_INTEGER;
expect(validate("openabtest-manifest", manifest).ok).toBe(false);
manifest.variants[0].weight = 1;
manifest.window.endsAt = manifest.window.startsAt;
expect(validate("openabtest-manifest", manifest).ok).toBe(false);
});
it("rejects client pricing authority and malformed discounts", () => {
const manifest = fixture("chovy-manifest");
manifest.assignment.authority = "client";
expect(validate("openabtest-manifest", manifest).ok).toBe(false);
manifest.assignment.authority = "server";
manifest.variants[0].parameters.discountBps = "500";
expect(validate("openabtest-manifest", manifest).ok).toBe(false);
});
it.each(["assignment", "exposure", "conversion"])("requires assignment attribution on %s", (name) => {
const event = fixture(name);
delete event.payload.assignmentId;
expect(validate("openabtest-event", event).ok).toBe(false);
});
it("rejects unrecognized event payloads and identifiable participant values", () => {
const event = fixture("exposure");
event.payload.email = "person@example.com";
expect(validate("openabtest-event", event).ok).toBe(false);
delete event.payload.email;
event.participantId = "person@example.com";
expect(validate("openabtest-event", event).ok).toBe(false);
});
it("checks the accepted discount and half-up usage arithmetic", () => {
const event = fixture("conversion");
event.payload.price.chargedUnitPriceMinor = 38000;
expect(validate("openabtest-event", event).ok).toBe(false);
Object.assign(event.payload.price, { listUnitPriceMinor: 101, chargedUnitPriceMinor: 91, quantityMilliUnits: 500, totalMinor: 46 });
expect(validate("openabtest-event", event).ok).toBe(true);
event.payload.price.totalMinor = 45;
expect(validate("openabtest-event", event).ok).toBe(false);
});
it("rejects a conversion dated before the accepted offer", () => {
const event = fixture("conversion");
event.at = "2026-09-14T00:00:00Z";
expect(validate("openabtest-event", event).ok).toBe(false);
});
it("requires a revenue reduction and evidence for refunds", () => {
const event = fixture("adjustment");
event.payload.amountMinor = 3600;
expect(validate("openabtest-event", event).ok).toBe(false);
event.payload.amountMinor = -3600;
event.payload.proofRefs = [];
expect(validate("openabtest-event", event).ok).toBe(false);
});
it("withholds payouts when actual costs or fees are missing", () => {
const event = fixture("reconciliation");
event.payload.actualCostMinor = null;
expect(validate("openabtest-event", event).ok).toBe(false);
event.payload.state = "pending";
expect(validate("openabtest-event", event).ok).toBe(false);
Object.assign(event.payload, { affiliateMinor: null, retainedProfitMinor: null, payoutStatus: "withheld" });
expect(validate("openabtest-event", event).ok).toBe(true);
});
it("rejects inconsistent accounting and unsupported profit claims", () => {
const event = fixture("reconciliation");
event.payload.affiliateMinor = 21000;
expect(validate("openabtest-event", event).ok).toBe(false);
event.payload.retainedProfitMinor = 4000;
expect(validate("openabtest-event", event).ok).toBe(false);
Object.assign(event.payload, { affiliateMinor: 0, actualCostMinor: 36000, retainedProfitMinor: -1000, payoutStatus: "withheld" });
expect(validate("openabtest-event", event).ok).toBe(true);
});
it("requires reconciliation evidence without bearer URLs", () => {
const event = fixture("reconciliation");
event.payload.proofRefs = [];
expect(validate("openabtest-event", event).ok).toBe(false);
event.payload.proofRefs = ["https://example.com/receipt?token=secret"];
expect(validate("openabtest-event", event).ok).toBe(false);
});
});

View file

@ -0,0 +1,68 @@
import type { ErrorObject } from "ajv";
type AccountingEvent =
| { kind: "conversion"; at: string; payload: { price: { listUnitPriceMinor: number; discountBps: number; quantityMilliUnits: number; chargedUnitPriceMinor: number; totalMinor: number; acceptedAt: string } } }
| { kind: "adjustment"; payload: { reason: string; amountMinor: number } }
| { kind: "reconciliation"; payload: { revenueMinor: number; refundsMinor: number; actualCostMinor: number | null; feesMinor: number | null; affiliateMinor: number | null; retainedProfitMinor: number | null; minimumRetainedProfitMinor: number; state: string; payoutStatus: string; proofRefs: string[] } };
// These checks run only after structural validation. Cross-event authorization,
// assignment lookup and idempotency still require the issuer's private ledger.
export function validateOpenABTest(kind: string, data: unknown): ErrorObject[] {
const errors: ErrorObject[] = [];
function report(path: string, message: string) {
errors.push({ keyword: "openabtest", instancePath: path, schemaPath: "#/openabtest-semantics", params: {}, message });
}
if (kind === "openabtest-manifest") {
const manifest = data as {
variants: Array<{ id: string; weight: number; parameters: Record<string, unknown> }>;
window: { startsAt: string; endsAt: string };
};
const seen = new Set<string>();
let total = 0n;
manifest.variants.forEach((variant, index) => {
if (seen.has(variant.id)) report(`/variants/${index}/id`, "must be unique within the manifest");
seen.add(variant.id);
total += BigInt(variant.weight);
const discount = variant.parameters.discountBps;
if (discount !== undefined && (typeof discount !== "number" || !Number.isInteger(discount) || discount < 0 || discount > 10000)) {
report(`/variants/${index}/parameters/discountBps`, "must be an integer from 0 to 10000");
}
});
if (total > BigInt(Number.MAX_SAFE_INTEGER)) report("/variants", "total weight must be a safe integer");
if (Date.parse(manifest.window.endsAt) <= Date.parse(manifest.window.startsAt)) {
report("/window/endsAt", "must be later than startsAt");
}
} else {
const event = data as AccountingEvent;
if (event.kind === "conversion") {
const price = event.payload.price;
const unit = (BigInt(price.listUnitPriceMinor) * BigInt(10000 - price.discountBps) + 5000n) / 10000n;
const total = (unit * BigInt(price.quantityMilliUnits) + 500n) / 1000n;
if (BigInt(price.chargedUnitPriceMinor) !== unit) report("/payload/price/chargedUnitPriceMinor", "must equal the discounted list unit price rounded half up");
if (BigInt(price.totalMinor) !== total) report("/payload/price/totalMinor", "must equal the charged unit price times quantity rounded half up");
if (Date.parse(price.acceptedAt) > Date.parse(event.at)) report("/payload/price/acceptedAt", "cannot be after the conversion event");
}
if (event.kind === "adjustment" && (event.payload.reason === "refund" || event.payload.reason === "chargeback") && event.payload.amountMinor >= 0) {
report("/payload/amountMinor", "refunds and chargebacks must reduce recognized revenue");
}
if (event.kind === "reconciliation") {
const p = event.payload;
if (p.refundsMinor > p.revenueMinor) report("/payload/refundsMinor", "cannot exceed revenue; other expenses belong in actual costs or fees");
if (p.state === "pending") {
if (p.payoutStatus !== "withheld" || p.affiliateMinor !== null || p.retainedProfitMinor !== null) {
report("/payload", "pending reconciliation must withhold payout and leave affiliate and retained profit unknown");
}
} else if (p.actualCostMinor === null || p.feesMinor === null || p.affiliateMinor === null || p.retainedProfitMinor === null) {
report("/payload", "reconciled accounting requires actual costs, fees, affiliate and retained profit");
} else {
const retained = BigInt(p.revenueMinor) - BigInt(p.refundsMinor) - BigInt(p.actualCostMinor) - BigInt(p.feesMinor) - BigInt(p.affiliateMinor);
if (retained !== BigInt(p.retainedProfitMinor)) report("/payload/retainedProfitMinor", "must reconcile exactly to net revenue less actual costs, fees and affiliate allocation");
if (p.proofRefs.length === 0) report("/payload/proofRefs", "reconciled accounting requires private evidence references");
if (p.retainedProfitMinor < p.minimumRetainedProfitMinor && (p.affiliateMinor !== 0 || p.payoutStatus !== "withheld")) {
report("/payload", "a profit-floor shortfall must withhold new payout and allocate zero new affiliate amount");
}
}
}
}
return errors;
}

View file

@ -1,3 +1,5 @@
import openabtestManifestSchema from "@logicsrc/schemas/openabtest-manifest" with { type: "json" };
import openabtestEventSchema from "@logicsrc/schemas/openabtest-event" with { type: "json" };
/**
* Every LogicSRC JSON Schema, keyed by kind.
*
@ -73,6 +75,8 @@ import openwallMessageSchema from "@logicsrc/schemas/openwall-message" with { ty
import openwallReceiptSchema from "@logicsrc/schemas/openwall-receipt" with { type: "json" };
export const schemas = {
"openabtest-manifest": openabtestManifestSchema,
"openabtest-event": openabtestEventSchema,
"openwall-message": openwallMessageSchema,
"openwall-receipt": openwallReceiptSchema,
agent: agentSchema,