From f971bd0e1ca4e31b676a9bc42883411f5ba56783 Mon Sep 17 00:00:00 2001 From: Isaac Martinez Date: Sat, 22 Aug 2026 12:01:58 -0400 Subject: [PATCH] feat(ledger): double-entry, where a stored balance cannot exist Phase 7A. 06 calls ledger-first "the single most consequential engineering decision in the system, and the one most likely to be compromised for convenience", so the shortcuts it warns about are made unavailable rather than discouraged. There is no balance column, so a balance cannot be stored. There is no method that writes a single entry, so nothing can post one side of a movement. There is no update and no delete, so history cannot be repaired in place. What remains is post, which writes a balanced transaction or writes nothing at all. Law XXI is the one worth reading the code for. 06: "an implementation that can produce a negative balance can mint currency." So every account a movement touches is checked after the entries are written and inside the same database transaction, with those accounts locked first. Without the lock two concurrent debits can each read a sufficient balance, each pass, and together overdraw, which is exactly the path that mints. There is a test that runs both at once. Amounts are bigint end to end, summed in Postgres and read back as text before being widened. A SUM over bigint that arrived as a JavaScript number would be the floating point error 06 forbids, entering through the one place nobody looks. A value past MAX_SAFE_INTEGER round-trips exactly, and there is a test for that too. Three things the domain enforced that this did not know about until the tests ran: an entry must carry its transaction's idempotency key, an entry must carry its transaction's type, and Kredbits cannot hold a negative value at all. The last one is why the overdraft check reads an unbranded bigint: the type cannot express the thing being detected, and branding first would raise a range error instead of reporting the overdraft. A finding rather than a gap: post cannot create the opening supply, because a transaction must sum to zero and creating money is precisely the movement that does not. Genesis belongs to the Central Bank, which is Phase 8. The tests seed an opening balance directly and say so. Verified against a real Postgres, including by mutation: removing the Law XXI check fails three tests, one of them the concurrent overdraft. --- packages/database/drizzle.config.ts | 1 + .../migrations/0005_tricky_random.sql | 135 ++ .../migrations/meta/0005_snapshot.json | 1730 +++++++++++++++++ .../database/migrations/meta/_journal.json | 7 + packages/database/src/index.ts | 1 + .../database/src/repositories/ledger.test.ts | 402 ++++ packages/database/src/repositories/ledger.ts | 255 +++ packages/database/src/schema/index.ts | 18 + packages/database/src/schema/ledger.ts | 294 +++ 9 files changed, 2843 insertions(+) create mode 100644 packages/database/migrations/0005_tricky_random.sql create mode 100644 packages/database/migrations/meta/0005_snapshot.json create mode 100644 packages/database/src/repositories/ledger.test.ts create mode 100644 packages/database/src/repositories/ledger.ts create mode 100644 packages/database/src/schema/ledger.ts diff --git a/packages/database/drizzle.config.ts b/packages/database/drizzle.config.ts index 4e2ac7b..ef57c91 100644 --- a/packages/database/drizzle.config.ts +++ b/packages/database/drizzle.config.ts @@ -17,6 +17,7 @@ export default defineConfig({ "./src/schema/github.ts", "./src/schema/events.ts", "./src/schema/contribution.ts", + "./src/schema/ledger.ts", ], out: "./migrations", dbCredentials: { url: process.env["DATABASE_URL"] ?? "" }, diff --git a/packages/database/migrations/0005_tricky_random.sql b/packages/database/migrations/0005_tricky_random.sql new file mode 100644 index 0000000..6c5ba22 --- /dev/null +++ b/packages/database/migrations/0005_tricky_random.sql @@ -0,0 +1,135 @@ +CREATE TYPE "public"."account_type" AS ENUM('CENTRAL_BANK_RESERVE', 'GLOBAL_WALLET', 'ORGANIZATION_POSITION', 'TREASURY', 'REVIEW_FUND', 'PENDING', 'NETWORK_RESERVE', 'PROTOCOL', 'BURNED');--> statement-breakpoint +CREATE TYPE "public"."currency_type" AS ENUM('KRED', 'LOCAL');--> statement-breakpoint +CREATE TYPE "public"."economy_type" AS ENUM('KREDS_NETWORK', 'SOVEREIGN_NETWORK', 'INDEPENDENT');--> statement-breakpoint +CREATE TYPE "public"."entry_direction" AS ENUM('DEBIT', 'CREDIT', 'MEMO');--> statement-breakpoint +CREATE TYPE "public"."entry_source_type" AS ENUM('PULL_REQUEST_MERGED', 'PULL_REQUEST_CLOSED', 'REVIEW_SUBMITTED', 'SETTLEMENT_RUN', 'TREASURY_OPERATION', 'CREDIT_OPERATION', 'NETWORK_OPERATION', 'MANUAL_ADJUSTMENT');--> statement-breakpoint +CREATE TYPE "public"."entry_status" AS ENUM('PENDING', 'SETTLED');--> statement-breakpoint +CREATE TYPE "public"."transaction_type" AS ENUM('DISTRIBUTION', 'TRANSFER', 'FEE', 'REFUND', 'REVERSAL', 'TREASURY_CONTRIBUTION', 'TREASURY_DISTRIBUTION', 'BURN', 'ADJUSTMENT', 'RESERVE_ALLOCATION', 'EXCHANGE', 'SETTLEMENT', 'REVIEW_FUND_CONTRIBUTION', 'REVIEW_FUND_PAYMENT', 'CREDIT_DRAW', 'DEBT_REPAYMENT', 'RECEIVABLE_CREATED', 'RECEIVABLE_SETTLED', 'RECEIVABLE_CANCELLED');--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "accounts" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "economy_id" uuid NOT NULL, + "type" "account_type" NOT NULL, + "owner_github_user_id" bigint, + "organization_id" uuid, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "currencies" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "economy_id" uuid NOT NULL, + "type" "currency_type" NOT NULL, + "code" text NOT NULL, + "name" text NOT NULL, + "subunits_per_unit" integer NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "economies" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "type" "economy_type" NOT NULL, + "organization_id" uuid, + "name" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "ledger_entries" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "transaction_id" uuid NOT NULL, + "economy_id" uuid NOT NULL, + "organization_id" uuid, + "account_id" uuid NOT NULL, + "direction" "entry_direction" NOT NULL, + "amount" bigint NOT NULL, + "type" "transaction_type" NOT NULL, + "source_type" "entry_source_type" NOT NULL, + "source_id" text NOT NULL, + "counterparty_account_id" uuid, + "rules_version" text NOT NULL, + "idempotency_key" text NOT NULL, + "status" "entry_status" DEFAULT 'PENDING' NOT NULL, + "settled_at" timestamp with time zone, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "ledger_transactions" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "economy_id" uuid NOT NULL, + "type" "transaction_type" NOT NULL, + "idempotency_key" text NOT NULL, + "rules_version" text NOT NULL, + "metadata" jsonb, + "created_at" timestamp with time zone DEFAULT now() NOT NULL, + CONSTRAINT "ledger_transactions_idempotency_key_unique" UNIQUE("idempotency_key") +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "accounts" ADD CONSTRAINT "accounts_economy_id_economies_id_fk" FOREIGN KEY ("economy_id") REFERENCES "public"."economies"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "accounts" ADD CONSTRAINT "accounts_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "currencies" ADD CONSTRAINT "currencies_economy_id_economies_id_fk" FOREIGN KEY ("economy_id") REFERENCES "public"."economies"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "economies" ADD CONSTRAINT "economies_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ledger_entries" ADD CONSTRAINT "ledger_entries_transaction_id_ledger_transactions_id_fk" FOREIGN KEY ("transaction_id") REFERENCES "public"."ledger_transactions"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ledger_entries" ADD CONSTRAINT "ledger_entries_economy_id_economies_id_fk" FOREIGN KEY ("economy_id") REFERENCES "public"."economies"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ledger_entries" ADD CONSTRAINT "ledger_entries_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ledger_entries" ADD CONSTRAINT "ledger_entries_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ledger_entries" ADD CONSTRAINT "ledger_entries_counterparty_account_id_accounts_id_fk" FOREIGN KEY ("counterparty_account_id") REFERENCES "public"."accounts"("id") ON DELETE set null ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "ledger_transactions" ADD CONSTRAINT "ledger_transactions_economy_id_economies_id_fk" FOREIGN KEY ("economy_id") REFERENCES "public"."economies"("id") ON DELETE restrict ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "accounts_economy_owner_idx" ON "accounts" USING btree ("economy_id","owner_github_user_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "accounts_economy_type_idx" ON "accounts" USING btree ("economy_id","type");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "accounts_organization_idx" ON "accounts" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "currencies_economy_idx" ON "currencies" USING btree ("economy_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "economies_organization_idx" ON "economies" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ledger_entries_account_idx" ON "ledger_entries" USING btree ("account_id","status");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ledger_entries_transaction_idx" ON "ledger_entries" USING btree ("transaction_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ledger_entries_economy_idx" ON "ledger_entries" USING btree ("economy_id","created_at");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ledger_entries_source_idx" ON "ledger_entries" USING btree ("source_type","source_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "ledger_transactions_economy_idx" ON "ledger_transactions" USING btree ("economy_id","created_at"); \ No newline at end of file diff --git a/packages/database/migrations/meta/0005_snapshot.json b/packages/database/migrations/meta/0005_snapshot.json new file mode 100644 index 0000000..9d4ee37 --- /dev/null +++ b/packages/database/migrations/meta/0005_snapshot.json @@ -0,0 +1,1730 @@ +{ + "id": "5e432fbf-2005-468f-a83a-dd42937daa5d", + "prevId": "74fa5157-8999-4dd1-b11d-dbcef7d0adce", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.github_identities": { + "name": "github_identities", + "schema": "", + "columns": { + "github_user_id": { + "name": "github_user_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "login": { + "name": "login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "actor_type": { + "name": "actor_type", + "type": "actor_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'UNKNOWN'" + }, + "status": { + "name": "status", + "type": "identity_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'UNCLAIMED'" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_identities_user_id_idx": { + "name": "github_identities_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_identities_login_idx": { + "name": "github_identities_login_idx", + "columns": [ + { + "expression": "login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "github_identities_user_id_users_id_fk": { + "name": "github_identities_user_id_users_id_fk", + "tableFrom": "github_identities", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.installations": { + "name": "installations", + "schema": "", + "columns": { + "github_installation_id": { + "name": "github_installation_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "installation_account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_github_id": { + "name": "account_github_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "installation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'ACTIVE'" + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "suspended_at": { + "name": "suspended_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "installations_organization_id_idx": { + "name": "installations_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "installations_organization_id_organizations_id_fk": { + "name": "installations_organization_id_organizations_id_fk", + "tableFrom": "installations", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "github_organization_id": { + "name": "github_organization_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "login": { + "name": "login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "connected_at": { + "name": "connected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_login_idx": { + "name": "organizations_login_idx", + "columns": [ + { + "expression": "login", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_github_organization_id_unique": { + "name": "organizations_github_organization_id_unique", + "nullsNotDistinct": false, + "columns": ["github_organization_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "github_repository_id": { + "name": "github_repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "github_installation_id": { + "name": "github_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name_with_owner": { + "name": "name_with_owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_private": { + "name": "is_private", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "is_personally_owned": { + "name": "is_personally_owned", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "trust_tier": { + "name": "trust_tier", + "type": "repository_trust_tier", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'UNTRUSTED'" + }, + "primary_branch": { + "name": "primary_branch", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'main'" + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "removed_at": { + "name": "removed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "relevance_score": { + "name": "relevance_score", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "relevance_breadth": { + "name": "relevance_breadth", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "relevance_signals": { + "name": "relevance_signals", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "relevance_measured_at": { + "name": "relevance_measured_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "repositories_installation_id_idx": { + "name": "repositories_installation_id_idx", + "columns": [ + { + "expression": "github_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "repositories_organization_id_idx": { + "name": "repositories_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "repositories_name_with_owner_idx": { + "name": "repositories_name_with_owner_idx", + "columns": [ + { + "expression": "name_with_owner", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "repositories_github_installation_id_installations_github_installation_id_fk": { + "name": "repositories_github_installation_id_installations_github_installation_id_fk", + "tableFrom": "repositories", + "tableTo": "installations", + "columnsFrom": ["github_installation_id"], + "columnsTo": ["github_installation_id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "repositories_organization_id_organizations_id_fk": { + "name": "repositories_organization_id_organizations_id_fk", + "tableFrom": "repositories", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repositories_github_repository_id_unique": { + "name": "repositories_github_repository_id_unique", + "nullsNotDistinct": false, + "columns": ["github_repository_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.domain_events": { + "name": "domain_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "domain_event_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "github_event_id": { + "name": "github_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "github_installation_id": { + "name": "github_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "domain_events_repository_idx": { + "name": "domain_events_repository_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "domain_events_type_idx": { + "name": "domain_events_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "domain_events_github_event_id_github_events_id_fk": { + "name": "domain_events_github_event_id_github_events_id_fk", + "tableFrom": "domain_events", + "tableTo": "github_events", + "columnsFrom": ["github_event_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "domain_events_repository_id_repositories_id_fk": { + "name": "domain_events_repository_id_repositories_id_fk", + "tableFrom": "domain_events", + "tableTo": "repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "domain_events_idempotency_key_unique": { + "name": "domain_events_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": ["idempotency_key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.github_events": { + "name": "github_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "github_delivery_id": { + "name": "github_delivery_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_installation_id": { + "name": "github_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "event_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'RECEIVED'" + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_events_status_idx": { + "name": "github_events_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "received_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_events_installation_idx": { + "name": "github_events_installation_idx", + "columns": [ + { + "expression": "github_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_events_type_idx": { + "name": "github_events_type_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "github_events_github_delivery_id_unique": { + "name": "github_events_github_delivery_id_unique", + "nullsNotDistinct": false, + "columns": ["github_delivery_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.contribution_entries": { + "name": "contribution_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entry_type": { + "name": "entry_type", + "type": "contribution_entry_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "contribution_kind", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "github_user_id": { + "name": "github_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "points": { + "name": "points", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "quality_score": { + "name": "quality_score", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "unobserved_signals": { + "name": "unobserved_signals", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger": { + "name": "trigger", + "type": "invalidation_trigger", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "cancels_entry_id": { + "name": "cancels_entry_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rules_version": { + "name": "rules_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "recorded_at": { + "name": "recorded_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "contribution_entries_user_idx": { + "name": "contribution_entries_user_idx", + "columns": [ + { + "expression": "github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contribution_entries_org_idx": { + "name": "contribution_entries_org_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "contribution_entries_repository_idx": { + "name": "contribution_entries_repository_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "contribution_entries_repository_id_repositories_id_fk": { + "name": "contribution_entries_repository_id_repositories_id_fk", + "tableFrom": "contribution_entries", + "tableTo": "repositories", + "columnsFrom": ["repository_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "contribution_entries_organization_id_organizations_id_fk": { + "name": "contribution_entries_organization_id_organizations_id_fk", + "tableFrom": "contribution_entries", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "contribution_entries_idempotency_key_unique": { + "name": "contribution_entries_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": ["idempotency_key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.accounts": { + "name": "accounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "economy_id": { + "name": "economy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "account_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "owner_github_user_id": { + "name": "owner_github_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "accounts_economy_owner_idx": { + "name": "accounts_economy_owner_idx", + "columns": [ + { + "expression": "economy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "owner_github_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_economy_type_idx": { + "name": "accounts_economy_type_idx", + "columns": [ + { + "expression": "economy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "accounts_organization_idx": { + "name": "accounts_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "accounts_economy_id_economies_id_fk": { + "name": "accounts_economy_id_economies_id_fk", + "tableFrom": "accounts", + "tableTo": "economies", + "columnsFrom": ["economy_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "accounts_organization_id_organizations_id_fk": { + "name": "accounts_organization_id_organizations_id_fk", + "tableFrom": "accounts", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.currencies": { + "name": "currencies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "economy_id": { + "name": "economy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "currency_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "subunits_per_unit": { + "name": "subunits_per_unit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "currencies_economy_idx": { + "name": "currencies_economy_idx", + "columns": [ + { + "expression": "economy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "currencies_economy_id_economies_id_fk": { + "name": "currencies_economy_id_economies_id_fk", + "tableFrom": "currencies", + "tableTo": "economies", + "columnsFrom": ["economy_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.economies": { + "name": "economies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "type": { + "name": "type", + "type": "economy_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "economies_organization_idx": { + "name": "economies_organization_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "economies_organization_id_organizations_id_fk": { + "name": "economies_organization_id_organizations_id_fk", + "tableFrom": "economies", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ledger_entries": { + "name": "ledger_entries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "transaction_id": { + "name": "transaction_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "economy_id": { + "name": "economy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "account_id": { + "name": "account_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "entry_direction", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "transaction_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "entry_source_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_id": { + "name": "source_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "counterparty_account_id": { + "name": "counterparty_account_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "rules_version": { + "name": "rules_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "entry_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'PENDING'" + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ledger_entries_account_idx": { + "name": "ledger_entries_account_idx", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_transaction_idx": { + "name": "ledger_entries_transaction_idx", + "columns": [ + { + "expression": "transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_economy_idx": { + "name": "ledger_entries_economy_idx", + "columns": [ + { + "expression": "economy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "ledger_entries_source_idx": { + "name": "ledger_entries_source_idx", + "columns": [ + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ledger_entries_transaction_id_ledger_transactions_id_fk": { + "name": "ledger_entries_transaction_id_ledger_transactions_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "ledger_transactions", + "columnsFrom": ["transaction_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "ledger_entries_economy_id_economies_id_fk": { + "name": "ledger_entries_economy_id_economies_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "economies", + "columnsFrom": ["economy_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "ledger_entries_organization_id_organizations_id_fk": { + "name": "ledger_entries_organization_id_organizations_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "organizations", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "ledger_entries_account_id_accounts_id_fk": { + "name": "ledger_entries_account_id_accounts_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "accounts", + "columnsFrom": ["account_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "ledger_entries_counterparty_account_id_accounts_id_fk": { + "name": "ledger_entries_counterparty_account_id_accounts_id_fk", + "tableFrom": "ledger_entries", + "tableTo": "accounts", + "columnsFrom": ["counterparty_account_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.ledger_transactions": { + "name": "ledger_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "economy_id": { + "name": "economy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "transaction_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "rules_version": { + "name": "rules_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "ledger_transactions_economy_idx": { + "name": "ledger_transactions_economy_idx", + "columns": [ + { + "expression": "economy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "ledger_transactions_economy_id_economies_id_fk": { + "name": "ledger_transactions_economy_id_economies_id_fk", + "tableFrom": "ledger_transactions", + "tableTo": "economies", + "columnsFrom": ["economy_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "ledger_transactions_idempotency_key_unique": { + "name": "ledger_transactions_idempotency_key_unique", + "nullsNotDistinct": false, + "columns": ["idempotency_key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.actor_type": { + "name": "actor_type", + "schema": "public", + "values": ["HUMAN", "BOT", "AI_AGENT", "UNKNOWN"] + }, + "public.identity_status": { + "name": "identity_status", + "schema": "public", + "values": ["CLAIMED", "UNCLAIMED", "RESTRICTED"] + }, + "public.installation_account_type": { + "name": "installation_account_type", + "schema": "public", + "values": ["ORGANIZATION", "USER"] + }, + "public.installation_status": { + "name": "installation_status", + "schema": "public", + "values": ["ACTIVE", "SUSPENDED", "REMOVED"] + }, + "public.repository_trust_tier": { + "name": "repository_trust_tier", + "schema": "public", + "values": ["UNTRUSTED", "ESTABLISHED", "RELEVANT", "HIGH_TRUST"] + }, + "public.domain_event_type": { + "name": "domain_event_type", + "schema": "public", + "values": [ + "PULL_REQUEST_MERGED", + "PULL_REQUEST_CLOSED", + "REVIEW_SUBMITTED", + "REPOSITORY_CONNECTED", + "REPOSITORY_DISCONNECTED" + ] + }, + "public.event_status": { + "name": "event_status", + "schema": "public", + "values": ["RECEIVED", "QUEUED", "PROCESSING", "PROCESSED", "FAILED", "IGNORED"] + }, + "public.contribution_entry_type": { + "name": "contribution_entry_type", + "schema": "public", + "values": ["AWARD", "INVALIDATION"] + }, + "public.contribution_kind": { + "name": "contribution_kind", + "schema": "public", + "values": ["PULL_REQUEST_MERGED", "CODE_REVIEW", "ISSUE_RESOLVED", "REVIEW_FOLLOW_UP"] + }, + "public.invalidation_trigger": { + "name": "invalidation_trigger", + "schema": "public", + "values": [ + "PR_REVERTED", + "CONFIRMED_FRAUD", + "CONFIRMED_FARMING", + "ACTOR_RECLASSIFIED_NON_HUMAN" + ] + }, + "public.account_type": { + "name": "account_type", + "schema": "public", + "values": [ + "CENTRAL_BANK_RESERVE", + "GLOBAL_WALLET", + "ORGANIZATION_POSITION", + "TREASURY", + "REVIEW_FUND", + "PENDING", + "NETWORK_RESERVE", + "PROTOCOL", + "BURNED" + ] + }, + "public.currency_type": { + "name": "currency_type", + "schema": "public", + "values": ["KRED", "LOCAL"] + }, + "public.economy_type": { + "name": "economy_type", + "schema": "public", + "values": ["KREDS_NETWORK", "SOVEREIGN_NETWORK", "INDEPENDENT"] + }, + "public.entry_direction": { + "name": "entry_direction", + "schema": "public", + "values": ["DEBIT", "CREDIT", "MEMO"] + }, + "public.entry_source_type": { + "name": "entry_source_type", + "schema": "public", + "values": [ + "PULL_REQUEST_MERGED", + "PULL_REQUEST_CLOSED", + "REVIEW_SUBMITTED", + "SETTLEMENT_RUN", + "TREASURY_OPERATION", + "CREDIT_OPERATION", + "NETWORK_OPERATION", + "MANUAL_ADJUSTMENT" + ] + }, + "public.entry_status": { + "name": "entry_status", + "schema": "public", + "values": ["PENDING", "SETTLED"] + }, + "public.transaction_type": { + "name": "transaction_type", + "schema": "public", + "values": [ + "DISTRIBUTION", + "TRANSFER", + "FEE", + "REFUND", + "REVERSAL", + "TREASURY_CONTRIBUTION", + "TREASURY_DISTRIBUTION", + "BURN", + "ADJUSTMENT", + "RESERVE_ALLOCATION", + "EXCHANGE", + "SETTLEMENT", + "REVIEW_FUND_CONTRIBUTION", + "REVIEW_FUND_PAYMENT", + "CREDIT_DRAW", + "DEBT_REPAYMENT", + "RECEIVABLE_CREATED", + "RECEIVABLE_SETTLED", + "RECEIVABLE_CANCELLED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/database/migrations/meta/_journal.json b/packages/database/migrations/meta/_journal.json index 2b97428..910193e 100644 --- a/packages/database/migrations/meta/_journal.json +++ b/packages/database/migrations/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1787413331784, "tag": "0004_hesitant_moon_knight", "breakpoints": true + }, + { + "idx": 5, + "version": "7", + "when": 1787414132672, + "tag": "0005_tricky_random", + "breakpoints": true } ] } diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index 367ebf5..a4a1cff 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -32,5 +32,6 @@ export { type InvalidationTrigger, type LeaderboardRow, } from "./repositories/contribution-ledger.js"; +export { InsufficientBalanceError, Ledger, type PostedTransaction } from "./repositories/ledger.js"; export { runMigrations } from "./migrate.js"; export * as schema from "./schema/index.js"; diff --git a/packages/database/src/repositories/ledger.test.ts b/packages/database/src/repositories/ledger.test.ts new file mode 100644 index 0000000..5729d98 --- /dev/null +++ b/packages/database/src/repositories/ledger.test.ts @@ -0,0 +1,402 @@ +import { afterAll, beforeEach, describe, expect, it } from "vitest"; +import { sql } from "drizzle-orm"; +import { dirname, join } from "node:path"; + +import { + accountId as toAccountId, + economyId as toEconomyId, + fromIso, + idempotencyKey as toKey, + kredbits, + ledgerEntryId as toEntryId, + rulesVersion as toRulesVersion, + transactionId as toTransactionId, + type LedgerEntry, + type Transaction, +} from "@kreds/domain"; + +import { createDatabase, type Database } from "../client.js"; +import { runMigrations } from "../migrate.js"; +import { InsufficientBalanceError, Ledger } from "./ledger.js"; + +const url = process.env["DATABASE_URL"]; +const describeWithDatabase = url ? describe : describe.skip; + +let db: Database; +let ledger: Ledger; + +const ECONOMY = "20000000-0000-0000-0000-000000000001"; +const RESERVE = "30000000-0000-0000-0000-000000000001"; +const ISAAC = "30000000-0000-0000-0000-000000000002"; +const JOSE = "30000000-0000-0000-0000-000000000003"; + +function entryOf(over: Partial & Pick) { + return { + id: toEntryId(crypto.randomUUID()), + economyId: toEconomyId(ECONOMY), + organizationId: null, + amount: kredbits(0n), + type: "TRANSFER" as const, + sourceType: "PULL_REQUEST_MERGED" as const, + sourceId: "PULL_REQUEST_MERGED:77001:412", + counterpartyAccountId: null, + rulesVersion: toRulesVersion("v0.4"), + idempotencyKey: toKey("entry"), + status: "SETTLED" as const, + settledAt: fromIso("2026-08-22T10:00:00Z"), + createdAt: fromIso("2026-08-22T10:00:00Z"), + metadata: {}, + ...over, + } as LedgerEntry; +} + +/** A two-sided movement: `from` is debited, `to` is credited. */ +function movement( + key: string, + from: string, + to: string, + amount: bigint, + over: Partial = {}, +): Transaction { + // The domain requires an entry to carry its transaction's type, so a + // distribution cannot contain an entry that reads as a transfer. + const type = (over.type ?? "TRANSFER") as Transaction["type"]; + return { + id: toTransactionId(crypto.randomUUID()), + type, + economyId: toEconomyId(ECONOMY), + rulesVersion: toRulesVersion("v0.4"), + idempotencyKey: toKey(key), + createdAt: fromIso("2026-08-22T10:00:00Z"), + entries: [ + entryOf({ + accountId: toAccountId(from), + direction: "DEBIT", + amount: kredbits(amount), + counterpartyAccountId: toAccountId(to), + // The domain requires every entry to carry its transaction's key, so + // one half of a movement cannot be deduplicated apart from the other. + idempotencyKey: toKey(key), + type, + }), + entryOf({ + accountId: toAccountId(to), + direction: "CREDIT", + amount: kredbits(amount), + counterpartyAccountId: toAccountId(from), + idempotencyKey: toKey(key), + type, + }), + ], + ...over, + } as Transaction; +} + +describeWithDatabase("Ledger", () => { + beforeEach(async () => { + db ??= createDatabase({ url: url as string, max: 4 }); + const here = dirname(new URL(import.meta.url).pathname); + await runMigrations(url as string, join(here, "..", "..", "migrations")); + await db.execute( + sql`truncate table ledger_entries, ledger_transactions, accounts, currencies, economies cascade`, + ); + await db.execute( + sql`insert into economies (id, type, name) values (${ECONOMY}, 'INDEPENDENT', 'test')`, + ); + for (const [id, type] of [ + [RESERVE, "CENTRAL_BANK_RESERVE"], + [ISAAC, "GLOBAL_WALLET"], + [JOSE, "GLOBAL_WALLET"], + ] as const) { + await db.execute( + sql`insert into accounts (id, economy_id, type) values (${id}, ${ECONOMY}, ${type})`, + ); + } + ledger = new Ledger(db); + }); + + afterAll(async () => { + await db?.$client.end({ timeout: 5 }); + }); + + describe("balances are derived", () => { + /** + * The single most consequential decision in the system, checked directly. + * There is no balance column, so there is nothing to read but the entries. + */ + it("has no balance column to read", async () => { + const columns = await db.execute<{ column_name: string }>( + sql`select column_name from information_schema.columns where table_name = 'accounts'`, + ); + expect(columns.map((c) => c.column_name)).not.toContain("balance"); + }); + + it("derives a balance from the entries that produced it", async () => { + await seed(3_500n); + expect(await ledger.balanceOf(ISAAC)).toBe(3_500n); + expect(await ledger.balanceOf(RESERVE)).toBe(10_000n - 3_500n); + }); + + it("reports zero for an account nothing has touched", async () => { + expect(await ledger.balanceOf(JOSE)).toBe(0n); + }); + }); + + describe("every movement balances", () => { + it("posts a two-sided transfer", async () => { + await seed(1_000n); + const posted = await ledger.post(movement("t-1", ISAAC, JOSE, 400n)); + + expect(posted.isNew).toBe(true); + expect(await ledger.balanceOf(ISAAC)).toBe(600n); + expect(await ledger.balanceOf(JOSE)).toBe(400n); + }); + + it("refuses entries that do not sum to zero, before touching the database", async () => { + const lopsided = movement("t-bad", ISAAC, JOSE, 100n); + const broken = { + ...lopsided, + entries: [ + lopsided.entries[0], + entryOf({ + accountId: toAccountId(JOSE), + direction: "CREDIT", + amount: kredbits(999n), + }), + ], + } as Transaction; + + await expect(ledger.post(broken)).rejects.toThrow(); + const [count] = await db.execute<{ count: string }>( + sql`select count(*)::text as count from ledger_transactions where idempotency_key = 't-bad'`, + ); + expect(count?.count).toBe("0"); + }); + + it("keeps the entries of a transaction retrievable, so it can be audited", async () => { + await seed(1_000n); + const posted = await ledger.post(movement("t-2", ISAAC, JOSE, 250n)); + const entries = await ledger.entriesOf(posted.id); + + expect(entries).toHaveLength(2); + const net = entries.reduce( + (sum, e) => sum + (e.direction === "CREDIT" ? e.amount : -e.amount), + 0n, + ); + expect(net).toBe(0n); + }); + }); + + describe("Law XXI, no monetary creation through debt", () => { + /** + * 06: "an implementation that can produce a negative balance can mint + * currency." This is the test that says it cannot. + */ + it("refuses a movement that would overdraw an account", async () => { + await seed(100n); + await expect(ledger.post(movement("t-over", ISAAC, JOSE, 500n))).rejects.toBeInstanceOf( + InsufficientBalanceError, + ); + }); + + /** The rollback matters as much as the refusal: a half-written movement is worse. */ + it("writes nothing at all when it refuses", async () => { + await seed(100n); + await ledger.post(movement("t-over2", ISAAC, JOSE, 500n)).catch(() => undefined); + + expect(await ledger.balanceOf(ISAAC)).toBe(100n); + expect(await ledger.balanceOf(JOSE)).toBe(0n); + const [count] = await db.execute<{ count: string }>( + sql`select count(*)::text as count from ledger_entries where source_id like '%412%' and transaction_id in (select id from ledger_transactions where idempotency_key = 't-over2')`, + ); + expect(count?.count).toBe("0"); + }); + + it("allows a movement that lands exactly on zero", async () => { + await seed(400n); + await ledger.post(movement("t-exact", ISAAC, JOSE, 400n)); + expect(await ledger.balanceOf(ISAAC)).toBe(0n); + }); + + /** + * The concurrency the lock exists for. Two debits that each look affordable + * alone must not both succeed: that is the path 06 says mints currency. + */ + it("does not let two concurrent debits together overdraw an account", async () => { + await seed(500n); + + const results = await Promise.allSettled([ + ledger.post(movement("race-a", ISAAC, JOSE, 400n)), + ledger.post(movement("race-b", ISAAC, JOSE, 400n)), + ]); + + const succeeded = results.filter((r) => r.status === "fulfilled").length; + expect(succeeded).toBe(1); + expect(await ledger.balanceOf(ISAAC)).toBe(100n); + }); + }); + + describe("idempotency", () => { + /** GitHub delivers at least once, so the same movement arrives more than once. */ + it("posts the same movement once", async () => { + await seed(1_000n); + const first = await ledger.post(movement("t-idem", ISAAC, JOSE, 300n)); + const again = await ledger.post(movement("t-idem", ISAAC, JOSE, 300n)); + + expect(again.isNew).toBe(false); + expect(again.id).toBe(first.id); + expect(await ledger.balanceOf(JOSE)).toBe(300n); + }); + + it("survives ten replays without paying ten times", async () => { + await seed(1_000n); + for (let i = 0; i < 10; i++) { + await ledger.post(movement("t-replay", ISAAC, JOSE, 250n)); + } + expect(await ledger.balanceOf(JOSE)).toBe(250n); + }); + }); + + describe("what settled and what merely happened", () => { + /** Law VII: earned is not withdrawable. */ + it("counts a pending entry in the balance but not in what settled", async () => { + await seed(1_000n); + await ledger.post( + movement("t-pending", ISAAC, JOSE, 200n, { + entries: [ + entryOf({ + accountId: toAccountId(ISAAC), + direction: "DEBIT", + amount: kredbits(200n), + status: "PENDING", + settledAt: null, + idempotencyKey: toKey("t-pending"), + }), + entryOf({ + accountId: toAccountId(JOSE), + direction: "CREDIT", + amount: kredbits(200n), + status: "PENDING", + settledAt: null, + idempotencyKey: toKey("t-pending"), + }), + ], + } as Partial), + ); + + expect(await ledger.balanceOf(JOSE)).toBe(200n); + expect(await ledger.settledBalanceOf(JOSE)).toBe(0n); + }); + }); + + describe("precision", () => { + /** + * 06: "Floating point silently loses value." A value past the safe integer + * range must round-trip exactly, which it cannot do through a JavaScript + * number. + */ + it("round-trips a value no double could hold", async () => { + const huge = 9_007_199_254_740_993n; // MAX_SAFE_INTEGER + 2 + await seedAmount(huge); + expect(await ledger.balanceOf(ISAAC)).toBe(huge); + }); + + it("never loses a subunit across many small movements", async () => { + await seed(1_000n); + for (let i = 0; i < 20; i++) { + await ledger.post(movement(`t-small-${i}`, ISAAC, JOSE, 7n)); + } + expect(await ledger.balanceOf(ISAAC)).toBe(1_000n - 140n); + expect(await ledger.balanceOf(JOSE)).toBe(140n); + // Conservation: what left one account arrived at the other, exactly. + expect((await ledger.balanceOf(ISAAC)) + (await ledger.balanceOf(JOSE))).toBe(1_000n); + }); + }); + + describe("history explains the balance", () => { + /** + * 06: "If a support ticket asks 'why do I have 1,240 K?', the answer must be + * reconstructible from entries, not from a support engineer's memory." + */ + it("reconstructs a balance from nothing but its entries", async () => { + await seed(1_000n); + await ledger.post(movement("h-1", ISAAC, JOSE, 300n)); + await ledger.post(movement("h-2", ISAAC, JOSE, 120n)); + + const entries = await ledger.entriesFor(ISAAC); + const reconstructed = entries.reduce( + (sum, e) => sum + (e.direction === "CREDIT" ? e.amount : -e.amount), + 0n, + ); + + expect(reconstructed).toBe(await ledger.balanceOf(ISAAC)); + expect(reconstructed).toBe(580n); + }); + + /** No update, no delete: history is repaired with new entries, never in place. */ + it("has exactly one way to write, and none to change or remove", () => { + // The exact surface, rather than a pattern. A regex here first flagged + // settledBalanceOf, which reads, and a guard that cries wolf is a guard + // somebody relaxes. Listing what may exist says the same thing and + // cannot be fooled by a name that merely looks like a verb. + const surface = Object.getOwnPropertyNames(Ledger.prototype) + .filter((name) => name !== "constructor" && !name.startsWith("_")) + .sort(); + + expect(surface).toEqual([ + "balanceOf", + "entriesFor", + "entriesOf", + "findByIdempotencyKey", + "netAmount", + "post", + "rawBalance", + "settledBalanceOf", + ]); + }); + }); + + /** + * Give the reserve an opening balance, directly. + * + * Not through `post`, and that is the finding rather than a shortcut: a + * transaction must sum to zero, and creating the money supply is precisely + * the movement that does not. Genesis belongs to the Central Bank, which is + * Phase 8. This fixture stands in for it so the ledger's own guarantees can + * be tested against something that already exists. + */ + async function openingBalance(amount: bigint): Promise { + await db.execute(sql` + insert into ledger_transactions (id, economy_id, type, idempotency_key, rules_version) + values (gen_random_uuid(), ${ECONOMY}, 'DISTRIBUTION', 'genesis', 'v0.4') + `); + await db.execute(sql` + insert into ledger_entries + (transaction_id, economy_id, account_id, direction, amount, type, + source_type, source_id, rules_version, idempotency_key, status, settled_at) + select id, ${ECONOMY}, ${RESERVE}, 'CREDIT', ${amount.toString()}::bigint, 'DISTRIBUTION', + 'NETWORK_OPERATION', 'genesis', 'v0.4', 'genesis-entry', 'SETTLED', now() + from ledger_transactions where idempotency_key = 'genesis' + `); + } + + /** Open the reserve, then issue from it to Isaac. */ + async function seed(amount: bigint): Promise { + await openingBalance(10_000n); + await ledger.post( + movement("seed-issue", RESERVE, ISAAC, amount, { + type: "DISTRIBUTION", + } as Partial), + ); + } + + /** The same, when the amount is larger than any sensible float. */ + async function seedAmount(amount: bigint): Promise { + await openingBalance(amount * 2n); + await ledger.post( + movement("seed-issue", RESERVE, ISAAC, amount, { + type: "DISTRIBUTION", + } as Partial), + ); + } +}); diff --git a/packages/database/src/repositories/ledger.ts b/packages/database/src/repositories/ledger.ts new file mode 100644 index 0000000..dd13f14 --- /dev/null +++ b/packages/database/src/repositories/ledger.ts @@ -0,0 +1,255 @@ +import { and, eq, inArray, sql } from "drizzle-orm"; +import { + kredbits, + transaction as validateTransaction, + type Kredbits, + type Transaction, +} from "@kreds/domain"; + +import type { Database } from "../client.js"; +import { accounts, ledgerEntries, ledgerTransactions } from "../schema/index.js"; + +/** + * Either the pool or an open transaction. + * + * The balance check has to run *inside* the transaction that wrote the entries, + * or it would read a state the rest of the world can already see and the whole + * lock would be decorative. + */ +type Queryable = Pick; + +/** + * Thrown when a movement would take an account below zero. + * + * Law XXI, No Monetary Creation Through Debt: "A negative economic position may + * represent a liability, but it may never create spendable Official KRED." Debt + * is a separate field, never a negative balance, and 06 is explicit that "an + * implementation that can produce a negative balance can mint currency." + * + * Its own error type rather than a generic one, because a caller has to be able + * to tell an overdraft from a database failure: the first is a decision the + * economy made and the second is an outage. + */ +export class InsufficientBalanceError extends Error { + constructor( + readonly accountId: string, + readonly shortfall: Kredbits, + ) { + super(`account ${accountId} would go negative by ${shortfall} subunits.`); + this.name = "InsufficientBalanceError"; + } +} + +export interface PostedTransaction { + readonly id: string; + readonly idempotencyKey: string; + /** False when this movement had already been posted. */ + readonly isNew: boolean; +} + +/** + * The ledger. + * + * 06 calls ledger-first "the single most consequential engineering decision in + * the system, and the one most likely to be compromised for convenience", so + * the shortcuts it warns about are made unavailable rather than discouraged: + * + * - There is no balance column, so a balance cannot be stored. + * - There is no method that writes a single entry, so nothing can post one side + * of a movement. + * - There is no update or delete, so history cannot be repaired in place. + * + * What remains is `post`, which writes a balanced transaction or writes nothing. + */ +export class Ledger { + constructor(private readonly db: Database) {} + + /** + * Post a balanced transaction, atomically. + * + * Three things have to hold, and all three are checked before anything is + * visible to anybody: + * + * 1. **The entries balance.** Validated by `@kreds/domain`, which enforces it + * per economy so that only an `EXCHANGE` may span two (Law X). + * 2. **No account goes negative.** Checked after the entries are written and + * inside the same database transaction, with the affected accounts locked + * first so a concurrent debit cannot slip between the check and the write. + * 3. **The same movement posts once.** The idempotency key is unique, and a + * repeat returns the original rather than raising. + * + * @throws InsufficientBalanceError when the movement would overdraw an + * account. The database transaction rolls back, so nothing is written. + */ + async post(candidate: Transaction): Promise { + // Balance and economy rules first, before touching the database. A + // malformed transaction should never reach a connection. + const validated = validateTransaction(candidate); + + const existing = await this.findByIdempotencyKey(validated.idempotencyKey); + if (existing) return { ...existing, isNew: false }; + + return this.db.transaction(async (tx) => { + const affected = [...new Set(validated.entries.map((entry) => entry.accountId))]; + + // Lock every affected account for the rest of this transaction. Without + // this, two concurrent debits can each read a sufficient balance, each + // pass the check, and together overdraw the account, which is the exact + // path 06 says can mint currency. + await tx + .select({ id: accounts.id }) + .from(accounts) + .where(inArray(accounts.id, affected)) + .for("update"); + + const [posted] = await tx + .insert(ledgerTransactions) + .values({ + economyId: validated.economyId, + type: validated.type, + idempotencyKey: validated.idempotencyKey, + rulesVersion: validated.rulesVersion, + metadata: null, + }) + .onConflictDoNothing({ target: ledgerTransactions.idempotencyKey }) + .returning(); + + if (!posted) { + // Another connection posted the same movement between the check above + // and this insert. That is the idempotency working, not a failure. + const raced = await this.findByIdempotencyKey(validated.idempotencyKey); + if (!raced) throw new Error(`transaction ${validated.idempotencyKey} vanished mid-post.`); + return { ...raced, isNew: false }; + } + + await tx.insert(ledgerEntries).values( + validated.entries.map((entry) => ({ + transactionId: posted.id, + economyId: entry.economyId, + organizationId: entry.organizationId, + accountId: entry.accountId, + direction: entry.direction, + amount: entry.amount, + type: entry.type, + sourceType: entry.sourceType, + sourceId: entry.sourceId, + counterpartyAccountId: entry.counterpartyAccountId, + rulesVersion: entry.rulesVersion, + idempotencyKey: entry.idempotencyKey, + status: entry.status, + settledAt: entry.settledAt === null ? null : new Date(entry.settledAt), + metadata: entry.metadata, + })), + ); + + // Law XXI, checked on every account this moved rather than only the ones + // it debited. A transaction type that credits somewhere it should not is + // still caught, and checking all of them costs one query. + for (const accountId of affected) { + // Read as a plain bigint, not as Kredbits. `kredbits()` refuses a + // negative value, which is the invariant working, and branding before + // the check would raise a RangeError instead of the overdraft this + // needs to report. The type cannot hold the thing being detected. + const balance = await this.rawBalance(tx, accountId); + if (balance < 0n) { + throw new InsufficientBalanceError(accountId, kredbits(-balance)); + } + } + + return { id: posted.id, idempotencyKey: posted.idempotencyKey, isNew: true }; + }); + } + + /** + * An account's balance, derived. + * + * Never stored. 06: "A stored balance is faster to read and vastly simpler to + * write. It is also unauditable, unreversible, and impossible to reconcile, + * and the moment it drifts from reality by one kredbit, there is no way to + * find out why." + */ + async balanceOf(accountId: string): Promise { + return kredbits(await this.rawBalance(this.db, accountId)); + } + + /** + * What has settled, which is not the same as what has been earned. + * + * Law VII, Extraction Is Not Guaranteed. A pending entry is value that + * happened and is still inside the settlement window, and treating the two as + * one number is how an economy pays out money it has not finished verifying. + */ + async settledBalanceOf(accountId: string): Promise { + const [row] = await this.db + .select({ total: this.netAmount() }) + .from(ledgerEntries) + .where(and(eq(ledgerEntries.accountId, accountId), eq(ledgerEntries.status, "SETTLED"))); + return kredbits(BigInt(row?.total ?? 0)); + } + + /** Every entry for one account, oldest first, so history explains the balance. */ + async entriesFor(accountId: string, limit = 200) { + return this.db + .select() + .from(ledgerEntries) + .where(eq(ledgerEntries.accountId, accountId)) + .orderBy(sql`${ledgerEntries.createdAt} asc`) + .limit(limit); + } + + /** The entries of one transaction, for auditing that it balanced. */ + async entriesOf(transactionId: string) { + return this.db + .select() + .from(ledgerEntries) + .where(eq(ledgerEntries.transactionId, transactionId)); + } + + private async findByIdempotencyKey( + key: string, + ): Promise<{ id: string; idempotencyKey: string } | null> { + const [row] = await this.db + .select({ id: ledgerTransactions.id, idempotencyKey: ledgerTransactions.idempotencyKey }) + .from(ledgerTransactions) + .where(eq(ledgerTransactions.idempotencyKey, key)) + .limit(1); + return row ?? null; + } + + /** + * The unbranded sum. + * + * `Kredbits` cannot be negative by construction, so a balance that has gone + * negative cannot be expressed in it. Everything that needs to *detect* one + * reads this and brands afterwards. + */ + private async rawBalance(db: Queryable, accountId: string): Promise { + const [row] = await db + .select({ total: this.netAmount() }) + .from(ledgerEntries) + .where(eq(ledgerEntries.accountId, accountId)); + return BigInt(row?.total ?? 0); + } + + /** + * Credits minus debits, in one place. + * + * `MEMO` entries are excluded because they move no KRED: `RECEIVABLE_CREATED` + * and `RECEIVABLE_CANCELLED` record a liability appearing and disappearing, + * and counting them would make a claim look like money. + * + * Summed in the database as `numeric` and read back as a string, then widened + * to `BigInt`. A `SUM` over `bigint` that came back as a JavaScript number + * would be the floating point error 06 forbids, arriving through the one + * place nobody looks. + */ + private netAmount() { + return sql`coalesce(sum( + case ${ledgerEntries.direction} + when 'CREDIT' then ${ledgerEntries.amount} + when 'DEBIT' then -${ledgerEntries.amount} + else 0 + end + ), 0)::text`; + } +} diff --git a/packages/database/src/schema/index.ts b/packages/database/src/schema/index.ts index a8713a0..db10462 100644 --- a/packages/database/src/schema/index.ts +++ b/packages/database/src/schema/index.ts @@ -25,3 +25,21 @@ export { contributionKind, invalidationTrigger, } from "./contribution.js"; +export { + accountRelations, + accountType, + accounts, + currencies, + currencyType, + economies, + economyRelations, + economyType, + entryDirection, + entrySourceType, + entryStatus, + ledgerEntries, + ledgerEntryRelations, + ledgerTransactionRelations, + ledgerTransactions, + transactionType, +} from "./ledger.js"; diff --git a/packages/database/src/schema/ledger.ts b/packages/database/src/schema/ledger.ts new file mode 100644 index 0000000..89bef45 --- /dev/null +++ b/packages/database/src/schema/ledger.ts @@ -0,0 +1,294 @@ +import { relations } from "drizzle-orm"; +import { + bigint, + index, + integer, + jsonb, + pgEnum, + pgTable, + text, + timestamp, + uuid, +} from "drizzle-orm/pg-core"; + +import { organizations } from "./github"; + +/** Mirrors `EconomyType` in `@kreds/domain`. See 14 to 16 on sovereign economies. */ +export const economyType = pgEnum("economy_type", [ + "KREDS_NETWORK", + "SOVEREIGN_NETWORK", + "INDEPENDENT", +]); + +/** Mirrors `CurrencyType`. Law X: official KRED and a local currency are not the same thing. */ +export const currencyType = pgEnum("currency_type", ["KRED", "LOCAL"]); + +/** Mirrors `AccountType`. */ +export const accountType = pgEnum("account_type", [ + "CENTRAL_BANK_RESERVE", + "GLOBAL_WALLET", + "ORGANIZATION_POSITION", + "TREASURY", + "REVIEW_FUND", + "PENDING", + "NETWORK_RESERVE", + "PROTOCOL", + "BURNED", +]); + +/** Mirrors `TransactionType`. 06: Ledger, Transaction types. */ +export const transactionType = pgEnum("transaction_type", [ + "DISTRIBUTION", + "TRANSFER", + "FEE", + "REFUND", + "REVERSAL", + "TREASURY_CONTRIBUTION", + "TREASURY_DISTRIBUTION", + "BURN", + "ADJUSTMENT", + "RESERVE_ALLOCATION", + "EXCHANGE", + "SETTLEMENT", + "REVIEW_FUND_CONTRIBUTION", + "REVIEW_FUND_PAYMENT", + "CREDIT_DRAW", + "DEBT_REPAYMENT", + "RECEIVABLE_CREATED", + "RECEIVABLE_SETTLED", + "RECEIVABLE_CANCELLED", +]); + +/** + * Mirrors `EntryDirection`. + * + * `MEMO` exists for the two entry types that move no KRED at all, + * `RECEIVABLE_CREATED` and `RECEIVABLE_CANCELLED`. 06 keeps them in the ledger + * rather than in a side table on purpose: "a claim that is invisible to the + * ledger is a claim nobody can audit." + */ +export const entryDirection = pgEnum("entry_direction", ["DEBIT", "CREDIT", "MEMO"]); + +/** Mirrors `EntrySourceType`. Traces every entry back to what caused it. */ +export const entrySourceType = pgEnum("entry_source_type", [ + "PULL_REQUEST_MERGED", + "PULL_REQUEST_CLOSED", + "REVIEW_SUBMITTED", + "SETTLEMENT_RUN", + "TREASURY_OPERATION", + "CREDIT_OPERATION", + "NETWORK_OPERATION", + "MANUAL_ADJUSTMENT", +]); + +/** Mirrors `EntryStatus`. Law VII: earned is not the same as withdrawable. */ +export const entryStatus = pgEnum("entry_status", ["PENDING", "SETTLED"]); + +/** + * An economy: the boundary a currency and its accounts live inside. + * + * Law IV makes a GitHub organization the default economic boundary, and Law X + * keeps official KRED separate from local currencies. Both are enforced by + * every account and entry naming the economy it belongs to. + */ +export const economies = pgTable( + "economies", + { + id: uuid("id").primaryKey().defaultRandom(), + type: economyType("type").notNull(), + /** Null for a network-level economy that belongs to no single organization. */ + organizationId: uuid("organization_id").references(() => organizations.id, { + onDelete: "set null", + }), + name: text("name").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [index("economies_organization_idx").on(table.organizationId)], +); + +/** + * A currency. + * + * `subunitsPerUnit` is stored rather than assumed. 06 says the subunit name may + * change but the integer requirement may not, and a local economy is free to + * choose its own scale. Nothing reads a hard-coded 100. + */ +export const currencies = pgTable( + "currencies", + { + id: uuid("id").primaryKey().defaultRandom(), + economyId: uuid("economy_id") + .notNull() + .references(() => economies.id, { onDelete: "restrict" }), + type: currencyType("type").notNull(), + /** `KRED`, `ZIT`. Display only. */ + code: text("code").notNull(), + name: text("name").notNull(), + subunitsPerUnit: integer("subunits_per_unit").notNull(), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [index("currencies_economy_idx").on(table.economyId)], +); + +/** + * An account. + * + * **There is deliberately no balance column.** + * + * 06: Ledger is blunt about why, and it is worth keeping the reason next to the + * absence: "A stored balance is faster to read and vastly simpler to write. It + * is also unauditable, unreversible, and impossible to reconcile, and the + * moment it drifts from reality by one kredbit, there is no way to find out + * why." + * + * Balances are derived from entries. Every time. The absence of the column is + * what makes that true rather than aspirational. + */ +export const accounts = pgTable( + "accounts", + { + id: uuid("id").primaryKey().defaultRandom(), + economyId: uuid("economy_id") + .notNull() + .references(() => economies.id, { onDelete: "restrict" }), + type: accountType("type").notNull(), + /** + * The GitHub identity this account belongs to, when it belongs to a person. + * + * Keyed on the identity rather than the Kreds account, for the same reason + * contributions are: Law XVII lets an identity earn before it has an + * account, so keying on the account would leave that value belonging to + * nobody. + */ + ownerGitHubUserId: bigint("owner_github_user_id", { mode: "number" }), + /** Enforces the organization boundary (Law IV). Null for network-level accounts. */ + organizationId: uuid("organization_id").references(() => organizations.id, { + onDelete: "set null", + }), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [ + index("accounts_economy_owner_idx").on(table.economyId, table.ownerGitHubUserId), + index("accounts_economy_type_idx").on(table.economyId, table.type), + index("accounts_organization_idx").on(table.organizationId), + ], +); + +/** + * A grouped, balanced set of entries. + * + * Nothing reaches the ledger except through one of these, which is what makes + * the balance invariant checkable: entries are written together or not at all, + * and a transaction whose entries do not sum to zero is rejected before any of + * it lands. + */ +export const ledgerTransactions = pgTable( + "ledger_transactions", + { + id: uuid("id").primaryKey().defaultRandom(), + economyId: uuid("economy_id") + .notNull() + .references(() => economies.id, { onDelete: "restrict" }), + type: transactionType("type").notNull(), + /** + * Makes a duplicate webhook harmless. + * + * 06: "GitHub explicitly delivers webhooks *at least once*: retries after + * timeouts, redeliveries, and duplicate events under load are normal + * operation, not failure." + */ + idempotencyKey: text("idempotency_key").notNull().unique(), + /** Law XV: makes history immune to policy changes. */ + rulesVersion: text("rules_version").notNull(), + metadata: jsonb("metadata"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [index("ledger_transactions_economy_idx").on(table.economyId, table.createdAt)], +); + +/** + * One side of a movement. + * + * `amount` is a non-negative integer count of subunits and `direction` carries + * the sign. Two columns rather than one signed number, because a query that + * forgot the sign would silently compute the wrong balance, and a reader + * scanning rows can see what each one did. + * + * `bigint` in `bigint` mode, so values round-trip as BigInt and never touch a + * JavaScript number. 06: "Floating point silently loses value. `0.1 + 0.2 !== + * 0.3` is a curiosity in a tutorial and a supply-conservation violation here." + */ +export const ledgerEntries = pgTable( + "ledger_entries", + { + id: uuid("id").primaryKey().defaultRandom(), + transactionId: uuid("transaction_id") + .notNull() + .references(() => ledgerTransactions.id, { onDelete: "restrict" }), + economyId: uuid("economy_id") + .notNull() + .references(() => economies.id, { onDelete: "restrict" }), + organizationId: uuid("organization_id").references(() => organizations.id, { + onDelete: "set null", + }), + accountId: uuid("account_id") + .notNull() + .references(() => accounts.id, { onDelete: "restrict" }), + direction: entryDirection("direction").notNull(), + /** Non-negative subunits. The sign lives in `direction`. */ + amount: bigint("amount", { mode: "bigint" }).notNull(), + type: transactionType("type").notNull(), + sourceType: entrySourceType("source_type").notNull(), + /** What caused this, traceable back to the GitHub event. */ + sourceId: text("source_id").notNull(), + /** Makes every transfer two-sided and reconcilable. */ + counterpartyAccountId: uuid("counterparty_account_id").references(() => accounts.id, { + onDelete: "set null", + }), + rulesVersion: text("rules_version").notNull(), + idempotencyKey: text("idempotency_key").notNull(), + status: entryStatus("status").notNull().default("PENDING"), + /** Law VII: separates earned from withdrawable. */ + settledAt: timestamp("settled_at", { withTimezone: true }), + metadata: jsonb("metadata"), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => [ + // Deriving a balance: every entry for one account. + index("ledger_entries_account_idx").on(table.accountId, table.status), + index("ledger_entries_transaction_idx").on(table.transactionId), + index("ledger_entries_economy_idx").on(table.economyId, table.createdAt), + // Tracing an entry back to what caused it. + index("ledger_entries_source_idx").on(table.sourceType, table.sourceId), + ], +); + +export const economyRelations = relations(economies, ({ many, one }) => ({ + currencies: many(currencies), + accounts: many(accounts), + organization: one(organizations, { + fields: [economies.organizationId], + references: [organizations.id], + }), +})); + +export const accountRelations = relations(accounts, ({ one, many }) => ({ + economy: one(economies, { fields: [accounts.economyId], references: [economies.id] }), + entries: many(ledgerEntries), +})); + +export const ledgerTransactionRelations = relations(ledgerTransactions, ({ many, one }) => ({ + entries: many(ledgerEntries), + economy: one(economies, { + fields: [ledgerTransactions.economyId], + references: [economies.id], + }), +})); + +export const ledgerEntryRelations = relations(ledgerEntries, ({ one }) => ({ + transaction: one(ledgerTransactions, { + fields: [ledgerEntries.transactionId], + references: [ledgerTransactions.id], + }), + account: one(accounts, { fields: [ledgerEntries.accountId], references: [accounts.id] }), +}));