Scaffold PIG and model the compute-GTM ontology

PIG is an agent-native CRM for two-sided AI-compute companies: businesses
that buy GPU capacity from providers and resell it. Their business is the
spread between two pipelines, which is precisely what a generic CRM cannot
represent.

The load-bearing decision is the `allocations` table, joining a
capacity_commitment (what we bought, at a known cost) to a demand_deal
(what we sold, at a known price). Margin, utilisation and idle capacity all
fall out of that one join. Cost is charged against the full commitment
rather than only the hours that sold, because unsold hours are already paid
for and any other treatment flatters a block that is losing money.

Domain decisions worth noting, each grounded in how this market operates:

- Demand stages put `legal` second, not last. Customers do not hand
  workloads to an infrastructure provider before paper is executed.
- Supply qualification splits technical from financial diligence, recorded
  attributably. Accepting capacity is a two-key decision.
- Capacity carries a time SHAPE (intervals + quantities), not a window.
  Commitments ramp and step down; a rectangle reports availability that
  does not exist in the month someone wants it.
- SLAs model three distinct shapes: none, a reliability tier plus credits
  policy, and a negotiated agreement. Aggregators generally cannot promise
  uptime on resold capacity, but negotiate heavyweight paper upstream.
  Remedies include fee abatement, which is materially better than a capped
  credit and is not expressible as one.
- Export control is a predicate on the allocation edge, evaluated against
  the ULTIMATE parent's jurisdiction. Country of incorporation is not a
  valid key, so this cannot live as a flag on an account.
- Agent-derived claims land in `facts` with a confidence band and evidence.
  Only verified claims self-apply; weaker ones await review.
- The API never calls the agent. It writes to a leased queue, guarded by a
  partial unique index on unfinished work.

Verified: typechecks clean, migration generates and applies to Postgres 16
(31 tables, 24 enums, 117 indexes).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 18:41:41 -07:00
commit d36762f264
33 changed files with 12342 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
{
"name": "@pig/core",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"zod": "^3.24.1"
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './ontology';
export * from './margin';
+158
View File
@@ -0,0 +1,158 @@
/**
* Margin arithmetic — the reason PIG exists.
*
* A two-sided compute business buys capacity in blocks (a commitment) and sells
* it in slices (allocations against deals). The spread between what a
* GPU-hour cost and what it sold for, net of the hours nobody bought, is the
* business. These functions are deliberately pure and unit-tested: every
* dashboard number and every agent answer resolves through them, so an error
* here is an error everywhere.
*
* All money is handled in minor units (cents) as integers. Floating-point
* currency in a system that reports margin is a defect waiting to be found by
* an accountant.
*/
/** A block of capacity purchased from a supplier. */
export interface CommitmentInput {
/** Total GPU-hours contracted over the term. */
gpuHours: number;
/** What we pay per GPU-hour, in cents. */
costPerGpuHourCents: number;
}
/** A slice of that block sold to a customer. */
export interface AllocationInput {
/** GPU-hours allocated to a demand deal. */
gpuHours: number;
/** What the customer pays per GPU-hour, in cents. */
pricePerGpuHourCents: number;
}
export interface MarginResult {
/** Hours bought. */
committedGpuHours: number;
/** Hours sold. */
allocatedGpuHours: number;
/** Hours bought and not sold. This is the number that hurts. */
idleGpuHours: number;
/** Share of committed capacity that is sold, 01. */
utilisation: number;
/** Total paid to the supplier, cents. */
costCents: number;
/** Total billed to customers, cents. */
revenueCents: number;
/**
* Revenue minus the FULL cost of the commitment — not merely the cost of the
* hours that sold. Unsold hours on a commitment are already paid for, so
* charging only allocated cost would flatter the number and hide the very
* problem this system exists to surface.
*/
grossMarginCents: number;
/** Gross margin as a share of revenue, 01. Null when there is no revenue. */
grossMarginPct: number | null;
/** Effective blended margin per GPU-hour sold, in cents. Null if nothing sold. */
marginPerAllocatedGpuHourCents: number | null;
}
export function computeMargin(
commitment: CommitmentInput,
allocations: readonly AllocationInput[],
): MarginResult {
const committedGpuHours = commitment.gpuHours;
const allocatedGpuHours = allocations.reduce((sum, a) => sum + a.gpuHours, 0);
// Over-allocation is possible and legitimate: capacity can be oversubscribed
// deliberately, on the assumption not every buyer uses their full reservation.
// Clamping idle at zero keeps the figure meaningful when that happens.
const idleGpuHours = Math.max(0, committedGpuHours - allocatedGpuHours);
const costCents = Math.round(committedGpuHours * commitment.costPerGpuHourCents);
const revenueCents = allocations.reduce(
(sum, a) => sum + Math.round(a.gpuHours * a.pricePerGpuHourCents),
0,
);
const grossMarginCents = revenueCents - costCents;
return {
committedGpuHours,
allocatedGpuHours,
idleGpuHours,
utilisation: committedGpuHours > 0 ? allocatedGpuHours / committedGpuHours : 0,
costCents,
revenueCents,
grossMarginCents,
grossMarginPct: revenueCents > 0 ? grossMarginCents / revenueCents : null,
marginPerAllocatedGpuHourCents:
allocatedGpuHours > 0 ? grossMarginCents / allocatedGpuHours : null,
};
}
/**
* The break-even sell price for the remaining unsold hours on a commitment.
*
* This is the number a seller actually wants mid-quarter: "the block is half
* sold and already paid for — what must I get for the rest to come out even?"
* It falls as more of the block sells, which is why it is computed against
* remaining hours rather than the whole commitment.
*
* Returns null when the block is fully allocated: there is nothing left to
* price, and dividing by zero hours would produce a confident-looking
* Infinity.
*/
export function breakEvenPricePerGpuHourCents(
commitment: CommitmentInput,
allocations: readonly AllocationInput[],
): number | null {
const m = computeMargin(commitment, allocations);
if (m.idleGpuHours <= 0) return null;
const uncoveredCents = m.costCents - m.revenueCents;
// Already in profit: any further sale is upside, so the floor is zero rather
// than a negative price, which would be nonsense to display.
if (uncoveredCents <= 0) return 0;
return uncoveredCents / m.idleGpuHours;
}
/**
* Aggregate margin across many commitments — a book-level view.
*
* Deliberately sums the underlying cents rather than averaging the per-block
* percentages: an average of ratios weights a tiny block equally with a huge
* one and produces a number that is wrong in the direction of whichever blocks
* happen to be small.
*/
export function aggregateMargin(
books: readonly { commitment: CommitmentInput; allocations: readonly AllocationInput[] }[],
): MarginResult {
const results = books.map((b) => computeMargin(b.commitment, b.allocations));
const committedGpuHours = results.reduce((s, r) => s + r.committedGpuHours, 0);
const allocatedGpuHours = results.reduce((s, r) => s + r.allocatedGpuHours, 0);
const idleGpuHours = results.reduce((s, r) => s + r.idleGpuHours, 0);
const costCents = results.reduce((s, r) => s + r.costCents, 0);
const revenueCents = results.reduce((s, r) => s + r.revenueCents, 0);
const grossMarginCents = revenueCents - costCents;
return {
committedGpuHours,
allocatedGpuHours,
idleGpuHours,
utilisation: committedGpuHours > 0 ? allocatedGpuHours / committedGpuHours : 0,
costCents,
revenueCents,
grossMarginCents,
grossMarginPct: revenueCents > 0 ? grossMarginCents / revenueCents : null,
marginPerAllocatedGpuHourCents:
allocatedGpuHours > 0 ? grossMarginCents / allocatedGpuHours : null,
};
}
/** Format cents as a currency string for display. */
export function formatCents(cents: number, currency = 'USD'): string {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency,
maximumFractionDigits: 2,
}).format(cents / 100);
}
+471
View File
@@ -0,0 +1,471 @@
/**
* PIG — the domain ontology for a two-sided AI-compute go-to-market team.
*
* This file is the single source of truth for the vocabulary of the product.
* The database schema, the API contracts, the UI, and the MCP tool surface all
* derive their enums from here, so that a stage or a tier means exactly one
* thing everywhere.
*
* Where a value set was taken from how this market demonstrably operates rather
* than invented, the source is cited in a comment. Fidelity to the real motion
* matters more than tidiness: a CRM whose stages do not match how deals
* actually move is abandoned within a quarter.
*/
// ---------------------------------------------------------------------------
// Teams
// ---------------------------------------------------------------------------
/**
* A two-sided compute business has three constituencies competing for the same
* scarce GPUs. Research is deliberately first-class: internal research burn is
* real capacity consumption, and margin math that cannot see it is wrong.
*/
export const TEAMS = ['supply', 'demand', 'research'] as const;
export type Team = (typeof TEAMS)[number];
export const TEAM_LABELS: Record<Team, string> = {
supply: 'Supply',
demand: 'Demand',
research: 'Research',
};
export const TEAM_DESCRIPTIONS: Record<Team, string> = {
supply: 'Sources, qualifies, prices and contracts GPU capacity from providers.',
demand: 'Sells compute and post-training; renews and expands accounts.',
research: 'Consumes capacity internally. Real burn, no revenue.',
};
/** Role within a team. Authorization is team-scoped, never global by default. */
export const TEAM_ROLES = ['member', 'lead', 'admin'] as const;
export type TeamRole = (typeof TEAM_ROLES)[number];
// ---------------------------------------------------------------------------
// Accounts
// ---------------------------------------------------------------------------
/**
* An account may sit on either side of the market — or both. Neoclouds
* routinely sell capacity to an aggregator while also buying managed training
* from it, so `both` is a real state and not a data-entry error.
*/
export const ACCOUNT_SIDES = ['supply', 'demand', 'both'] as const;
export type AccountSide = (typeof ACCOUNT_SIDES)[number];
/**
* Supply-side counterparty archetypes. These behave very differently on price,
* lead time and contract weight, so the type drives qualification defaults.
*/
export const SUPPLIER_TYPES = [
'hyperscaler', // AWS, Azure, GCP, OCI — deep capacity, rigid terms
'neocloud', // GPU-first clouds — the core supply base
'regional_operator', // Sovereign / in-country operators; jurisdiction matters
'datacenter', // Colocation and bare-metal facility owners
'broker', // Resellers and capacity brokers; margin stacks
'depin', // Decentralised / crypto-adjacent compute networks
'community', // Long-tail individual GPU owners
] as const;
export type SupplierType = (typeof SUPPLIER_TYPES)[number];
/**
* Demand-side segments. Buying behaviour differs sharply: frontier labs
* negotiate multi-year reserved capacity, applied startups burst on-demand,
* and sovereign buyers bring procurement processes measured in quarters.
*/
export const CUSTOMER_SEGMENTS = [
'frontier_lab',
'applied_ai_startup',
'enterprise',
'research_institution',
'sovereign',
'individual_developer',
] as const;
export type CustomerSegment = (typeof CUSTOMER_SEGMENTS)[number];
// ---------------------------------------------------------------------------
// Product lines
// ---------------------------------------------------------------------------
/**
* A single account can carry several independent opportunities across product
* lines. Modelling every deal as "GPU hours" collapses that and makes
* land-and-expand invisible, which is precisely the motion these businesses
* run.
*/
export const PRODUCT_LINES = [
'compute_ondemand',
'compute_reserved',
'compute_spot',
'post_training', // RL / fine-tuning / hosted training
'inference',
'evaluations',
'platform', // Hosted tooling subscriptions
] as const;
export type ProductLine = (typeof PRODUCT_LINES)[number];
// ---------------------------------------------------------------------------
// Pipelines
// ---------------------------------------------------------------------------
/**
* Demand pipeline.
*
* Sourced from how enterprise AI-infrastructure deals are publicly described as
* progressing: qualification, legal, scoping, proposal, procurement, POC,
* deployment, expansion.
*
* Note that `legal` sits SECOND. In this market MSA and DPA execution gates the
* engagement rather than concluding it — a customer will not hand workloads to
* an infrastructure provider before paper is in place. Generic CRMs place
* contracting at the end of the funnel and are simply wrong about it here.
*/
export const DEMAND_STAGES = [
'qualification',
'legal',
'scoping',
'proposal',
'procurement',
'poc',
'deployment',
'expansion',
'closed_won',
'closed_lost',
] as const;
export type DemandStage = (typeof DEMAND_STAGES)[number];
export const DEMAND_STAGE_LABELS: Record<DemandStage, string> = {
qualification: 'Qualification',
legal: 'Legal',
scoping: 'Scoping',
proposal: 'Proposal',
procurement: 'Procurement',
poc: 'POC',
deployment: 'Deployment',
expansion: 'Expansion',
closed_won: 'Closed won',
closed_lost: 'Closed lost',
};
/** Stages that represent a live, forecastable opportunity. */
export const DEMAND_OPEN_STAGES: readonly DemandStage[] = [
'qualification',
'legal',
'scoping',
'proposal',
'procurement',
'poc',
'deployment',
'expansion',
];
/**
* Supply pipeline.
*
* Qualification is split into `technical_diligence` and `financial_diligence`
* on purpose. Accepting capacity is a two-key decision — engineering judges
* whether the cluster can actually train (interconnect, storage, reliability
* history) while finance judges whether the economics clear. Collapsing them
* into one "qualified" stage loses the record of who accepted what and why.
*/
export const SUPPLY_STAGES = [
'sourced',
'qualifying',
'technical_diligence',
'financial_diligence',
'pricing',
'contracting',
'onboarding',
'live',
'renewal',
'churned',
'rejected',
] as const;
export type SupplyStage = (typeof SUPPLY_STAGES)[number];
export const SUPPLY_STAGE_LABELS: Record<SupplyStage, string> = {
sourced: 'Sourced',
qualifying: 'Qualifying',
technical_diligence: 'Technical diligence',
financial_diligence: 'Financial diligence',
pricing: 'Pricing',
contracting: 'Contracting',
onboarding: 'Onboarding',
live: 'Live',
renewal: 'Renewal',
churned: 'Churned',
rejected: 'Rejected',
};
export const SUPPLY_OPEN_STAGES: readonly SupplyStage[] = [
'sourced',
'qualifying',
'technical_diligence',
'financial_diligence',
'pricing',
'contracting',
'onboarding',
];
// ---------------------------------------------------------------------------
// Hardware
// ---------------------------------------------------------------------------
/**
* GPU model identifiers follow the Prime Intellect availability API's
* convention (`H100_80GB`, `B200`, …) so that synced inventory maps across
* without translation. The list is open — unknown values are stored verbatim
* rather than rejected, because new accelerators appear faster than schemas
* are updated.
*/
export const KNOWN_GPU_TYPES = [
'GB200',
'B300',
'B200',
'H200',
'H100_80GB',
'GH200',
'A100_80GB',
'A100_40GB',
'L40S',
'A40',
'RTX_4090',
'RTX_5090',
] as const;
export type KnownGpuType = (typeof KNOWN_GPU_TYPES)[number];
/** Socket type. SXM implies NVLink and therefore multi-node training viability. */
export const GPU_SOCKETS = ['PCIe', 'SXM2', 'SXM3', 'SXM4', 'SXM5', 'SXM6'] as const;
export type GpuSocket = (typeof GPU_SOCKETS)[number];
/**
* Interconnect is the field that decides whether a cluster can train or can
* only serve. Ethernet-only capacity at a training price is the most common way
* to be overcharged in this market.
*/
export const INTERCONNECT_TYPES = [
'Infiniband',
'RoCE',
'NVLink',
'Ethernet',
'Unknown',
] as const;
export type InterconnectType = (typeof INTERCONNECT_TYPES)[number];
/**
* Reliability tier.
*
* This is how compute aggregators express reliability when they cannot offer a
* conventional uptime SLA — because they resell third-party infrastructure they
* do not control. Secure-cloud capacity sits in vetted datacenters; community
* capacity is cheaper and less dependable. See `SlaKind` below for how this
* interacts with contractual commitments.
*/
export const SECURITY_TIERS = ['secure_cloud', 'community_cloud'] as const;
export type SecurityTier = (typeof SECURITY_TIERS)[number];
/** Scarcity signal on a listing. Drives "sell this now" alerts. */
export const STOCK_STATUSES = [
'Available',
'Low',
'Medium',
'High',
'Unavailable',
] as const;
export type StockStatus = (typeof STOCK_STATUSES)[number];
// ---------------------------------------------------------------------------
// Contracts
// ---------------------------------------------------------------------------
export const CONTRACT_TYPES = [
'msa', // Master Service Agreement — the umbrella
'dpa', // Data Processing Agreement — required for most enterprise buyers
'sla', // Service Level Agreement — see SLA_KINDS
'order_form', // The commercial specifics under an MSA
'capacity_commitment', // Reserved capacity purchased from a supplier
'nda',
'amendment',
] as const;
export type ContractType = (typeof CONTRACT_TYPES)[number];
export const CONTRACT_STATUSES = [
'draft',
'in_review',
'in_negotiation',
'out_for_signature',
'executed',
'expired',
'terminated',
] as const;
export type ContractStatus = (typeof CONTRACT_STATUSES)[number];
/**
* How service levels are actually promised in this market.
*
* A compute aggregator generally CANNOT offer a conventional uptime guarantee
* on resold capacity, and says so publicly. What it offers instead is a
* reliability tier plus a credits policy. But the same company will negotiate
* heavyweight SLAs on the supply side — reserved capacity agreements, MSAs,
* DPAs and order forms at very large scale — and will negotiate bespoke SLAs
* with enterprise customers on dedicated clusters.
*
* So all three shapes coexist and the schema must hold them without pretending
* one is the others:
*
* `none` Self-serve. No service commitment at all.
* `credits_policy` Reliability tier + service credits on provider failure.
* `negotiated` A real, signed SLA with committed metrics.
*/
export const SLA_KINDS = ['none', 'credits_policy', 'negotiated'] as const;
export type SlaKind = (typeof SLA_KINDS)[number];
/** Committed metrics on a negotiated SLA. */
export const SLA_METRICS = [
'uptime_pct',
'node_availability_pct',
'node_replacement_hours',
'mttr_hours',
'support_response_hours',
'throughput_floor',
] as const;
export type SlaMetric = (typeof SLA_METRICS)[number];
// ---------------------------------------------------------------------------
// Evidence
// ---------------------------------------------------------------------------
/**
* Every agent-derived fact carries a confidence band. Strong signals apply
* automatically; weak ones become proposals for a human to accept or dismiss.
*
* This is what separates a trustworthy CRM from a store of plausible-sounding
* fabrications. An agent that can write unattributed claims into the record
* will eventually write a wrong one, and nobody will be able to tell which.
*/
export const FACT_BANDS = ['verified', 'probable', 'possible'] as const;
export type FactBand = (typeof FACT_BANDS)[number];
export const FACT_STATUSES = [
'applied', // Written to the record
'proposed', // Awaiting human review
'dismissed', // Rejected by a human
'superseded', // Replaced by a newer fact
] as const;
export type FactStatus = (typeof FACT_STATUSES)[number];
/** Confidence thresholds governing whether a fact applies or is merely proposed. */
export const FACT_BAND_THRESHOLDS: Record<FactBand, number> = {
verified: 0.9,
probable: 0.65,
possible: 0,
};
export function bandForScore(score: number): FactBand {
if (score >= FACT_BAND_THRESHOLDS.verified) return 'verified';
if (score >= FACT_BAND_THRESHOLDS.probable) return 'probable';
return 'possible';
}
/** Only `verified` facts write themselves. Everything else asks first. */
export function statusForBand(band: FactBand): FactStatus {
return band === 'verified' ? 'applied' : 'proposed';
}
// ---------------------------------------------------------------------------
// Activities
// ---------------------------------------------------------------------------
export const ACTIVITY_TYPES = [
'note',
'email',
'call',
'meeting',
'slack',
'buzz',
'stage_change',
'contract_event',
'task',
'agent_action',
] as const;
export type ActivityType = (typeof ACTIVITY_TYPES)[number];
// ---------------------------------------------------------------------------
// Agent task queue
// ---------------------------------------------------------------------------
/**
* Work Piggy can be asked to do. The API writes rows of these; it never calls
* the agent directly. The queue therefore survives the agent being offline, and
* restarting the agent replays nothing that already finished.
*/
export const AGENT_TASK_KINDS = [
'enrich_account',
'enrich_contact',
'write_brief',
'match_capacity',
'detect_idle_capacity',
'summarise_pipeline',
'watch_renewal',
'research_supplier',
] as const;
export type AgentTaskKind = (typeof AGENT_TASK_KINDS)[number];
export const AGENT_TASK_OUTCOMES = [
'succeeded',
'failed',
'skipped',
'cancelled',
] as const;
export type AgentTaskOutcome = (typeof AGENT_TASK_OUTCOMES)[number];
// ---------------------------------------------------------------------------
// Provenance
// ---------------------------------------------------------------------------
/**
* How a record entered PIG. Seeded records are public research and are graded,
* never presented as established fact.
*/
export const RECORD_SOURCES = [
'manual',
'seed',
'agent',
'prime_api',
'slack',
'buzz',
'import',
] as const;
export type RecordSource = (typeof RECORD_SOURCES)[number];
/**
* Confidence in a seeded or enriched record about a real person or company.
*
* `unverified` is a first-class value and must be shown in the UI. Presenting
* a single-source claim about a real person with the same visual weight as a
* corroborated one is how a CRM quietly becomes misinformation.
*/
export const CONFIDENCE_GRADES = [
'confirmed', // Two or more independent sources
'probable', // One good source
'unverified', // Single weak or self-reported source
'disputed', // Sources conflict
] as const;
export type ConfidenceGrade = (typeof CONFIDENCE_GRADES)[number];
/**
* Relationship of a person to the organisation they are filed under. Being
* named in a company's repository or paper does not make someone an employee,
* and a CRM that conflates the two will embarrass whoever acts on it.
*/
export const AFFILIATION_KINDS = [
'staff',
'founder',
'alumni',
'contributor', // Open-source contributor, not employed
'resident', // Residency or internship programme
'advisor',
'investor',
'customer_reference',
'unknown',
] as const;
export type AffiliationKind = (typeof AFFILIATION_KINDS)[number];
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist",
"noEmit": true
},
"include": ["src/**/*.ts"]
}
+12
View File
@@ -0,0 +1,12 @@
import type { Config } from 'drizzle-kit';
export default {
schema: './src/schema/index.ts',
out: './migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL ?? 'postgres://pig:pig@localhost:5432/pig',
},
strict: true,
verbose: true,
} satisfies Config;
+771
View File
@@ -0,0 +1,771 @@
CREATE TYPE "public"."pig_account_side" AS ENUM('supply', 'demand', 'both');--> statement-breakpoint
CREATE TYPE "public"."pig_activity_type" AS ENUM('note', 'email', 'call', 'meeting', 'slack', 'buzz', 'stage_change', 'contract_event', 'task', 'agent_action');--> statement-breakpoint
CREATE TYPE "public"."pig_affiliation_kind" AS ENUM('staff', 'founder', 'alumni', 'contributor', 'resident', 'advisor', 'investor', 'customer_reference', 'unknown');--> statement-breakpoint
CREATE TYPE "public"."pig_agent_task_kind" AS ENUM('enrich_account', 'enrich_contact', 'write_brief', 'match_capacity', 'detect_idle_capacity', 'summarise_pipeline', 'watch_renewal', 'research_supplier');--> statement-breakpoint
CREATE TYPE "public"."pig_agent_task_outcome" AS ENUM('succeeded', 'failed', 'skipped', 'cancelled');--> statement-breakpoint
CREATE TYPE "public"."pig_confidence_grade" AS ENUM('confirmed', 'probable', 'unverified', 'disputed');--> statement-breakpoint
CREATE TYPE "public"."pig_contract_status" AS ENUM('draft', 'in_review', 'in_negotiation', 'out_for_signature', 'executed', 'expired', 'terminated');--> statement-breakpoint
CREATE TYPE "public"."pig_contract_type" AS ENUM('msa', 'dpa', 'sla', 'order_form', 'capacity_commitment', 'nda', 'amendment');--> statement-breakpoint
CREATE TYPE "public"."pig_customer_segment" AS ENUM('frontier_lab', 'applied_ai_startup', 'enterprise', 'research_institution', 'sovereign', 'individual_developer');--> statement-breakpoint
CREATE TYPE "public"."pig_demand_stage" AS ENUM('qualification', 'legal', 'scoping', 'proposal', 'procurement', 'poc', 'deployment', 'expansion', 'closed_won', 'closed_lost');--> statement-breakpoint
CREATE TYPE "public"."pig_fact_band" AS ENUM('verified', 'probable', 'possible');--> statement-breakpoint
CREATE TYPE "public"."pig_fact_status" AS ENUM('applied', 'proposed', 'dismissed', 'superseded');--> statement-breakpoint
CREATE TYPE "public"."pig_gpu_socket" AS ENUM('PCIe', 'SXM2', 'SXM3', 'SXM4', 'SXM5', 'SXM6');--> statement-breakpoint
CREATE TYPE "public"."pig_interconnect_type" AS ENUM('Infiniband', 'RoCE', 'NVLink', 'Ethernet', 'Unknown');--> statement-breakpoint
CREATE TYPE "public"."pig_product_line" AS ENUM('compute_ondemand', 'compute_reserved', 'compute_spot', 'post_training', 'inference', 'evaluations', 'platform');--> statement-breakpoint
CREATE TYPE "public"."pig_record_source" AS ENUM('manual', 'seed', 'agent', 'prime_api', 'slack', 'buzz', 'import');--> statement-breakpoint
CREATE TYPE "public"."pig_security_tier" AS ENUM('secure_cloud', 'community_cloud');--> statement-breakpoint
CREATE TYPE "public"."pig_sla_kind" AS ENUM('none', 'credits_policy', 'negotiated');--> statement-breakpoint
CREATE TYPE "public"."pig_sla_metric" AS ENUM('uptime_pct', 'node_availability_pct', 'node_replacement_hours', 'mttr_hours', 'support_response_hours', 'throughput_floor');--> statement-breakpoint
CREATE TYPE "public"."pig_stock_status" AS ENUM('Available', 'Low', 'Medium', 'High', 'Unavailable');--> statement-breakpoint
CREATE TYPE "public"."pig_supplier_type" AS ENUM('hyperscaler', 'neocloud', 'regional_operator', 'datacenter', 'broker', 'depin', 'community');--> statement-breakpoint
CREATE TYPE "public"."pig_supply_stage" AS ENUM('sourced', 'qualifying', 'technical_diligence', 'financial_diligence', 'pricing', 'contracting', 'onboarding', 'live', 'renewal', 'churned', 'rejected');--> statement-breakpoint
CREATE TYPE "public"."pig_team" AS ENUM('supply', 'demand', 'research');--> statement-breakpoint
CREATE TYPE "public"."pig_team_role" AS ENUM('member', 'lead', 'admin');--> statement-breakpoint
CREATE TABLE "api_keys" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" uuid NOT NULL,
"name" text NOT NULL,
"key_hash" text NOT NULL,
"key_prefix" text NOT NULL,
"scopes" jsonb DEFAULT '["read"]'::jsonb NOT NULL,
"last_used_at" timestamp with time zone,
"expires_at" timestamp with time zone,
"revoked_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "invites" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"code_hash" text NOT NULL,
"email" text,
"team" "pig_team",
"role" "pig_team_role" DEFAULT 'member' NOT NULL,
"created_by_user_id" uuid,
"expires_at" timestamp with time zone,
"uses_remaining" jsonb DEFAULT '1'::jsonb NOT NULL,
"redeemed_by_user_id" uuid,
"redeemed_at" timestamp with time zone,
"revoked_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "team_memberships" (
"user_id" uuid NOT NULL,
"team" "pig_team" NOT NULL,
"role" "pig_team_role" DEFAULT 'member' NOT NULL,
"is_primary" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
CONSTRAINT "team_memberships_user_id_team_pk" PRIMARY KEY("user_id","team")
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"auth_subject" uuid,
"email" text NOT NULL,
"name" text NOT NULL,
"handle" text,
"avatar_url" text,
"title" text,
"timezone" text,
"is_platform_admin" boolean DEFAULT false NOT NULL,
"deactivated_at" timestamp with time zone,
"last_seen_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "account_stats" (
"account_id" uuid PRIMARY KEY NOT NULL,
"open_deals" integer DEFAULT 0 NOT NULL,
"contact_count" integer DEFAULT 0 NOT NULL,
"refreshed_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "accounts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
"domain" text,
"website" text,
"description" text,
"logo_url" text,
"side" "pig_account_side" DEFAULT 'demand' NOT NULL,
"supplier_type" "pig_supplier_type",
"customer_segment" "pig_customer_segment",
"country" text,
"region" text,
"jurisdiction" text,
"ultimate_parent_account_id" uuid,
"ultimate_parent_name" text,
"ultimate_parent_country" text,
"ownership_verified_at" timestamp with time zone,
"linkedin_url" text,
"twitter_url" text,
"github_org" text,
"owner_user_id" uuid,
"source" "pig_record_source" DEFAULT 'manual' NOT NULL,
"source_url" text,
"confidence" "pig_confidence_grade" DEFAULT 'confirmed' NOT NULL,
"last_activity_at" timestamp with time zone,
"archived_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "activities" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"type" "pig_activity_type" NOT NULL,
"subject" text,
"body" text,
"account_id" uuid,
"contact_id" uuid,
"demand_deal_id" uuid,
"supply_deal_id" uuid,
"actor_user_id" uuid,
"actor_agent" text,
"occurred_at" timestamp with time zone DEFAULT now() NOT NULL,
"meta" jsonb,
"external_id" text,
"source" "pig_record_source" DEFAULT 'manual' NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "channel_links" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"platform" text NOT NULL,
"channel_id" text NOT NULL,
"channel_name" text,
"account_id" uuid,
"notify_on" jsonb DEFAULT '[]'::jsonb NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "contacts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"account_id" uuid,
"full_name" text NOT NULL,
"first_name" text,
"last_name" text,
"title" text,
"email" text,
"phone" text,
"linkedin_url" text,
"twitter_handle" text,
"github_handle" text,
"website_url" text,
"avatar_url" text,
"affiliation" "pig_affiliation_kind" DEFAULT 'unknown' NOT NULL,
"departed_at" timestamp with time zone,
"is_decision_maker" boolean DEFAULT false NOT NULL,
"owner_user_id" uuid,
"source" "pig_record_source" DEFAULT 'manual' NOT NULL,
"source_url" text,
"confidence" "pig_confidence_grade" DEFAULT 'confirmed' NOT NULL,
"confidence_note" text,
"last_activity_at" timestamp with time zone,
"archived_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "capacity_commitments" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"account_id" uuid NOT NULL,
"site_id" uuid,
"supply_deal_id" uuid,
"name" text NOT NULL,
"gpu_type" text NOT NULL,
"socket" "pig_gpu_socket",
"gpu_count" integer NOT NULL,
"interconnect_type" "pig_interconnect_type" DEFAULT 'Unknown' NOT NULL,
"security_tier" "pig_security_tier" DEFAULT 'secure_cloud' NOT NULL,
"starts_at" timestamp with time zone NOT NULL,
"ends_at" timestamp with time zone NOT NULL,
"total_gpu_hours" numeric(16, 2) NOT NULL,
"cost_per_gpu_hour_cents" integer NOT NULL,
"currency" text DEFAULT 'USD' NOT NULL,
"shape" jsonb,
"colocate_with" jsonb DEFAULT '[]'::jsonb NOT NULL,
"is_contiguous" boolean DEFAULT true NOT NULL,
"minimum_spend_cents" integer,
"is_auto_renew" boolean DEFAULT false NOT NULL,
"notice_days" integer,
"take_or_pay_floor_pct" numeric(6, 2),
"prepaid_pct" numeric(6, 2),
"prepaid_amount_cents" integer,
"useful_life_years" numeric(4, 2),
"salvage_value_pct" numeric(6, 2),
"depreciation_start_at" timestamp with time zone,
"cost_of_capital_bps" integer,
"financing_instrument" text,
"oversubscription_pct" numeric(6, 2) DEFAULT '0' NOT NULL,
"notes" text,
"terminated_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "inventory_listings" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"account_id" uuid,
"site_id" uuid,
"external_cloud_id" text,
"provider_slug" text,
"gpu_type" text NOT NULL,
"socket" "pig_gpu_socket",
"gpu_count" integer NOT NULL,
"gpu_memory_gb" integer,
"vcpu" integer,
"memory_gb" integer,
"disk_gb" integer,
"internet_mbps" integer,
"interconnect_gbps" integer,
"interconnect_type" "pig_interconnect_type" DEFAULT 'Unknown' NOT NULL,
"region" text,
"country" text,
"security_tier" "pig_security_tier" DEFAULT 'secure_cloud' NOT NULL,
"stock_status" "pig_stock_status" DEFAULT 'Available' NOT NULL,
"is_spot" boolean DEFAULT false NOT NULL,
"provisioning_minutes" integer,
"prepaid_hours" numeric(12, 2),
"on_demand_price_cents" integer,
"community_price_cents" integer,
"price_is_variable" boolean DEFAULT false NOT NULL,
"currency" text DEFAULT 'USD' NOT NULL,
"images" jsonb DEFAULT '[]'::jsonb NOT NULL,
"raw" jsonb,
"source" "pig_record_source" DEFAULT 'manual' NOT NULL,
"observed_at" timestamp with time zone DEFAULT now() NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "sites" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"account_id" uuid NOT NULL,
"name" text NOT NULL,
"external_data_center_id" text,
"country" text,
"country_code" text,
"region" text,
"city" text,
"jurisdiction" text,
"power_mw" numeric(10, 3),
"pue" numeric(4, 2),
"certifications" jsonb DEFAULT '[]'::jsonb NOT NULL,
"uptime_history_pct" numeric(6, 3),
"notes" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "supply_deals" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"account_id" uuid NOT NULL,
"site_id" uuid,
"name" text NOT NULL,
"stage" "pig_supply_stage" DEFAULT 'sourced' NOT NULL,
"stage_changed_at" timestamp with time zone DEFAULT now() NOT NULL,
"owner_user_id" uuid,
"primary_contact_id" uuid,
"gpu_type" text,
"gpu_count" integer,
"interconnect_type" "pig_interconnect_type",
"target_cost_per_gpu_hour_cents" integer,
"term_months" integer,
"available_from" timestamp with time zone,
"technical_verdict" text,
"technical_verdict_by" uuid,
"technical_verdict_at" timestamp with time zone,
"technical_notes" text,
"financial_verdict" text,
"financial_verdict_by" uuid,
"financial_verdict_at" timestamp with time zone,
"financial_notes" text,
"rejection_reason" text,
"closed_at" timestamp with time zone,
"last_activity_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "capacity_requests" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"demand_deal_id" uuid NOT NULL,
"gpu_type" text,
"gpu_type_alternatives" jsonb DEFAULT '[]'::jsonb NOT NULL,
"gpu_count" integer NOT NULL,
"requires_high_speed_interconnect" boolean DEFAULT false NOT NULL,
"min_interconnect_type" "pig_interconnect_type",
"min_security_tier" "pig_security_tier" DEFAULT 'secure_cloud' NOT NULL,
"allowed_regions" jsonb DEFAULT '[]'::jsonb NOT NULL,
"excluded_jurisdictions" jsonb DEFAULT '[]'::jsonb NOT NULL,
"starts_at" timestamp with time zone,
"ends_at" timestamp with time zone,
"total_gpu_hours" numeric(16, 2),
"max_price_per_gpu_hour_cents" integer,
"required_certifications" jsonb DEFAULT '[]'::jsonb NOT NULL,
"notes" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "deal_contacts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"demand_deal_id" uuid NOT NULL,
"contact_id" uuid NOT NULL,
"role" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "demand_deals" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"account_id" uuid NOT NULL,
"name" text NOT NULL,
"description" text,
"product_line" "pig_product_line" DEFAULT 'compute_reserved' NOT NULL,
"stage" "pig_demand_stage" DEFAULT 'qualification' NOT NULL,
"stage_changed_at" timestamp with time zone DEFAULT now() NOT NULL,
"owner_user_id" uuid,
"primary_contact_id" uuid,
"acv_cents" integer,
"tcv_cents" integer,
"currency" text DEFAULT 'USD' NOT NULL,
"term_months" integer,
"probability" numeric(4, 3),
"expected_close_date" timestamp with time zone,
"closed_at" timestamp with time zone,
"closed_reason" text,
"msa_executed" boolean DEFAULT false NOT NULL,
"dpa_executed" boolean DEFAULT false NOT NULL,
"parent_deal_id" uuid,
"last_activity_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "allocations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"capacity_commitment_id" uuid NOT NULL,
"demand_deal_id" uuid,
"internal_team" text,
"gpu_hours" numeric(16, 2) NOT NULL,
"price_per_gpu_hour_cents" integer DEFAULT 0 NOT NULL,
"currency" text DEFAULT 'USD' NOT NULL,
"starts_at" timestamp with time zone NOT NULL,
"ends_at" timestamp with time zone NOT NULL,
"status" text DEFAULT 'planned' NOT NULL,
"hold_expires_at" timestamp with time zone,
"hold_opportunity_cost_cents" integer,
"guarantee_type" text DEFAULT 'committed' NOT NULL,
"priority" integer DEFAULT 100 NOT NULL,
"compliance_decision_id" uuid,
"created_by_user_id" uuid,
"notes" text,
"released_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "contract_obligations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"contract_id" uuid NOT NULL,
"title" text NOT NULL,
"description" text,
"kind" text DEFAULT 'milestone' NOT NULL,
"due_at" timestamp with time zone NOT NULL,
"completed_at" timestamp with time zone,
"owner_user_id" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "contracts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"account_id" uuid NOT NULL,
"type" "pig_contract_type" NOT NULL,
"status" "pig_contract_status" DEFAULT 'draft' NOT NULL,
"side" text DEFAULT 'demand' NOT NULL,
"title" text NOT NULL,
"external_reference" text,
"demand_deal_id" uuid,
"supply_deal_id" uuid,
"capacity_commitment_id" uuid,
"parent_contract_id" uuid,
"contracting_party_name" text,
"take_or_pay_floor_pct" numeric(6, 2),
"prepaid_pct" numeric(6, 2),
"termination_tier" text,
"assignable_on_default" boolean DEFAULT false NOT NULL,
"assignment_deadline_business_days" integer,
"effective_at" timestamp with time zone,
"expires_at" timestamp with time zone,
"executed_at" timestamp with time zone,
"terminated_at" timestamp with time zone,
"is_auto_renew" boolean DEFAULT false NOT NULL,
"notice_days" integer,
"value_cents" integer,
"currency" text DEFAULT 'USD' NOT NULL,
"governing_law" text,
"document_url" text,
"owner_user_id" uuid,
"notes" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "sla_metric_targets" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"sla_term_id" uuid NOT NULL,
"metric" "pig_sla_metric" NOT NULL,
"target_value" numeric(12, 3) NOT NULL,
"unit" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "sla_terms" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"contract_id" uuid NOT NULL,
"kind" "pig_sla_kind" DEFAULT 'credits_policy' NOT NULL,
"uptime_target_pct" numeric(6, 3),
"node_replacement_hours" integer,
"mttr_hours" integer,
"support_response_hours" integer,
"measurement_window" text DEFAULT 'monthly' NOT NULL,
"measurement_unit" text DEFAULT 'cluster' NOT NULL,
"remedy_type" text DEFAULT 'service_credit' NOT NULL,
"abatement_trigger_value" integer,
"abatement_trigger_unit" text,
"claim_deadline_value" integer,
"claim_deadline_unit" text DEFAULT 'days' NOT NULL,
"credit_expiry_months" integer,
"is_sole_remedy" boolean DEFAULT true NOT NULL,
"spare_pool_obligation" text,
"spare_pool_scope" jsonb DEFAULT '[]'::jsonb NOT NULL,
"maintenance_classes" jsonb DEFAULT '[]'::jsonb NOT NULL,
"reasonable_endeavours_days_per_year" integer,
"rca_delivery_hours" integer,
"credit_schedule" jsonb DEFAULT '[]'::jsonb NOT NULL,
"credit_cap_pct" numeric(6, 3),
"exclusions" text,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "compliance_artifacts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"account_id" uuid NOT NULL,
"claim" text NOT NULL,
"scope" text,
"is_certified" boolean DEFAULT false NOT NULL,
"soc2_type" text,
"observation_window_start" timestamp with time zone,
"observation_window_end" timestamp with time zone,
"audit_firm" text,
"carve_out_method" text,
"products_in_scope" jsonb DEFAULT '[]'::jsonb NOT NULL,
"evidence_url" text,
"verified_by_user_id" uuid,
"verified_at" timestamp with time zone,
"expires_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "compliance_decisions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"allocation_id" uuid,
"account_id" uuid NOT NULL,
"beneficial_owner_name" text,
"ultimate_parent_country" text,
"physical_jurisdiction" text,
"end_use" text,
"decision" text DEFAULT 'needs_review' NOT NULL,
"rationale" text,
"rule_version" text,
"decided_by_user_id" uuid,
"decided_at" timestamp with time zone,
"re_evaluation_triggers" jsonb DEFAULT '[]'::jsonb NOT NULL,
"superseded_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "export_authorizations" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"account_id" uuid NOT NULL,
"authorization_type" text DEFAULT 'none' NOT NULL,
"reference" text,
"scope_notes" text,
"issued_at" timestamp with time zone,
"expires_at" timestamp with time zone,
"evidence_url" text,
"verified_by_user_id" uuid,
"verified_at" timestamp with time zone,
"volatile" boolean DEFAULT false NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "agent_actions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"agent_run_id" uuid,
"type" text NOT NULL,
"target_type" text,
"target_id" text,
"summary" text,
"idempotency_key" text NOT NULL,
"status" text DEFAULT 'pending' NOT NULL,
"external_id" text,
"error" text,
"metadata" jsonb,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "agent_runs" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"agent_task_id" uuid,
"agent" text DEFAULT 'piggy' NOT NULL,
"principal_user_id" uuid,
"status" text DEFAULT 'running' NOT NULL,
"model" text,
"input_tokens" integer,
"output_tokens" integer,
"cost_micro_cents" integer,
"input" jsonb,
"result" jsonb,
"summary" text,
"error" text,
"started_at" timestamp with time zone DEFAULT now() NOT NULL,
"finished_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "agent_tasks" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"kind" "pig_agent_task_kind" NOT NULL,
"subject" text NOT NULL,
"reason" text,
"payload" jsonb,
"priority" integer DEFAULT 0 NOT NULL,
"budget" integer DEFAULT 4 NOT NULL,
"attempts" integer DEFAULT 0 NOT NULL,
"max_attempts" integer DEFAULT 3 NOT NULL,
"due_at" timestamp with time zone DEFAULT now() NOT NULL,
"leased_until" timestamp with time zone,
"leased_by" text,
"started_at" timestamp with time zone,
"finished_at" timestamp with time zone,
"outcome" "pig_agent_task_outcome",
"error" text,
"requested_by_user_id" uuid,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "facts" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"account_id" uuid,
"contact_id" uuid,
"field" text NOT NULL,
"value" text NOT NULL,
"score" numeric(4, 3) NOT NULL,
"band" "pig_fact_band" NOT NULL,
"status" "pig_fact_status" DEFAULT 'proposed' NOT NULL,
"evidence" jsonb,
"source_url" text,
"method" text,
"agent_run_id" uuid,
"decided_by_user_id" uuid,
"decided_at" timestamp with time zone,
"observed_at" timestamp with time zone DEFAULT now() NOT NULL,
"superseded_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "field_definitions" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"entity" text NOT NULL,
"key" text NOT NULL,
"label" text NOT NULL,
"type" text DEFAULT 'text' NOT NULL,
"agent_filled" boolean DEFAULT true NOT NULL,
"agent_brief" text,
"is_required" boolean DEFAULT false NOT NULL,
"show_on_table" boolean DEFAULT false NOT NULL,
"position" integer DEFAULT 0 NOT NULL,
"archived_at" timestamp with time zone,
"created_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
CREATE TABLE "field_options" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"field_id" uuid NOT NULL,
"label" text NOT NULL,
"position" integer DEFAULT 0 NOT NULL,
"archived_at" timestamp with time zone
);
--> statement-breakpoint
CREATE TABLE "field_values" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"field_id" uuid NOT NULL,
"account_id" uuid,
"contact_id" uuid,
"demand_deal_id" uuid,
"supply_deal_id" uuid,
"text_value" text,
"number_value" numeric(24, 4),
"date_value" timestamp with time zone,
"bool_value" boolean,
"option_id" uuid,
"user_value" uuid,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invites" ADD CONSTRAINT "invites_created_by_user_id_users_id_fk" FOREIGN KEY ("created_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "invites" ADD CONSTRAINT "invites_redeemed_by_user_id_users_id_fk" FOREIGN KEY ("redeemed_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "team_memberships" ADD CONSTRAINT "team_memberships_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "account_stats" ADD CONSTRAINT "account_stats_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "activities" ADD CONSTRAINT "activities_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "activities" ADD CONSTRAINT "activities_contact_id_contacts_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."contacts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "activities" ADD CONSTRAINT "activities_actor_user_id_users_id_fk" FOREIGN KEY ("actor_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "channel_links" ADD CONSTRAINT "channel_links_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "contacts" ADD CONSTRAINT "contacts_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "contacts" ADD CONSTRAINT "contacts_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "capacity_commitments" ADD CONSTRAINT "capacity_commitments_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "capacity_commitments" ADD CONSTRAINT "capacity_commitments_site_id_sites_id_fk" FOREIGN KEY ("site_id") REFERENCES "public"."sites"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "capacity_commitments" ADD CONSTRAINT "capacity_commitments_supply_deal_id_supply_deals_id_fk" FOREIGN KEY ("supply_deal_id") REFERENCES "public"."supply_deals"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "inventory_listings" ADD CONSTRAINT "inventory_listings_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "inventory_listings" ADD CONSTRAINT "inventory_listings_site_id_sites_id_fk" FOREIGN KEY ("site_id") REFERENCES "public"."sites"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "sites" ADD CONSTRAINT "sites_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "supply_deals" ADD CONSTRAINT "supply_deals_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "supply_deals" ADD CONSTRAINT "supply_deals_site_id_sites_id_fk" FOREIGN KEY ("site_id") REFERENCES "public"."sites"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "supply_deals" ADD CONSTRAINT "supply_deals_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "supply_deals" ADD CONSTRAINT "supply_deals_primary_contact_id_contacts_id_fk" FOREIGN KEY ("primary_contact_id") REFERENCES "public"."contacts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "supply_deals" ADD CONSTRAINT "supply_deals_technical_verdict_by_users_id_fk" FOREIGN KEY ("technical_verdict_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "supply_deals" ADD CONSTRAINT "supply_deals_financial_verdict_by_users_id_fk" FOREIGN KEY ("financial_verdict_by") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "capacity_requests" ADD CONSTRAINT "capacity_requests_demand_deal_id_demand_deals_id_fk" FOREIGN KEY ("demand_deal_id") REFERENCES "public"."demand_deals"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "deal_contacts" ADD CONSTRAINT "deal_contacts_demand_deal_id_demand_deals_id_fk" FOREIGN KEY ("demand_deal_id") REFERENCES "public"."demand_deals"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "deal_contacts" ADD CONSTRAINT "deal_contacts_contact_id_contacts_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."contacts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "demand_deals" ADD CONSTRAINT "demand_deals_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "demand_deals" ADD CONSTRAINT "demand_deals_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "demand_deals" ADD CONSTRAINT "demand_deals_primary_contact_id_contacts_id_fk" FOREIGN KEY ("primary_contact_id") REFERENCES "public"."contacts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "allocations" ADD CONSTRAINT "allocations_capacity_commitment_id_capacity_commitments_id_fk" FOREIGN KEY ("capacity_commitment_id") REFERENCES "public"."capacity_commitments"("id") ON DELETE restrict ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "allocations" ADD CONSTRAINT "allocations_demand_deal_id_demand_deals_id_fk" FOREIGN KEY ("demand_deal_id") REFERENCES "public"."demand_deals"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "allocations" ADD CONSTRAINT "allocations_created_by_user_id_users_id_fk" FOREIGN KEY ("created_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "contract_obligations" ADD CONSTRAINT "contract_obligations_contract_id_contracts_id_fk" FOREIGN KEY ("contract_id") REFERENCES "public"."contracts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "contract_obligations" ADD CONSTRAINT "contract_obligations_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_demand_deal_id_demand_deals_id_fk" FOREIGN KEY ("demand_deal_id") REFERENCES "public"."demand_deals"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_supply_deal_id_supply_deals_id_fk" FOREIGN KEY ("supply_deal_id") REFERENCES "public"."supply_deals"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_capacity_commitment_id_capacity_commitments_id_fk" FOREIGN KEY ("capacity_commitment_id") REFERENCES "public"."capacity_commitments"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "sla_metric_targets" ADD CONSTRAINT "sla_metric_targets_sla_term_id_sla_terms_id_fk" FOREIGN KEY ("sla_term_id") REFERENCES "public"."sla_terms"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "sla_terms" ADD CONSTRAINT "sla_terms_contract_id_contracts_id_fk" FOREIGN KEY ("contract_id") REFERENCES "public"."contracts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "compliance_artifacts" ADD CONSTRAINT "compliance_artifacts_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "compliance_artifacts" ADD CONSTRAINT "compliance_artifacts_verified_by_user_id_users_id_fk" FOREIGN KEY ("verified_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "compliance_decisions" ADD CONSTRAINT "compliance_decisions_allocation_id_allocations_id_fk" FOREIGN KEY ("allocation_id") REFERENCES "public"."allocations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "compliance_decisions" ADD CONSTRAINT "compliance_decisions_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "compliance_decisions" ADD CONSTRAINT "compliance_decisions_decided_by_user_id_users_id_fk" FOREIGN KEY ("decided_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "export_authorizations" ADD CONSTRAINT "export_authorizations_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "export_authorizations" ADD CONSTRAINT "export_authorizations_verified_by_user_id_users_id_fk" FOREIGN KEY ("verified_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "agent_actions" ADD CONSTRAINT "agent_actions_agent_run_id_agent_runs_id_fk" FOREIGN KEY ("agent_run_id") REFERENCES "public"."agent_runs"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "agent_runs" ADD CONSTRAINT "agent_runs_agent_task_id_agent_tasks_id_fk" FOREIGN KEY ("agent_task_id") REFERENCES "public"."agent_tasks"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "agent_runs" ADD CONSTRAINT "agent_runs_principal_user_id_users_id_fk" FOREIGN KEY ("principal_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "agent_tasks" ADD CONSTRAINT "agent_tasks_requested_by_user_id_users_id_fk" FOREIGN KEY ("requested_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "facts" ADD CONSTRAINT "facts_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "facts" ADD CONSTRAINT "facts_contact_id_contacts_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."contacts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "facts" ADD CONSTRAINT "facts_decided_by_user_id_users_id_fk" FOREIGN KEY ("decided_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "field_options" ADD CONSTRAINT "field_options_field_id_field_definitions_id_fk" FOREIGN KEY ("field_id") REFERENCES "public"."field_definitions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "field_values" ADD CONSTRAINT "field_values_field_id_field_definitions_id_fk" FOREIGN KEY ("field_id") REFERENCES "public"."field_definitions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "field_values" ADD CONSTRAINT "field_values_account_id_accounts_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "field_values" ADD CONSTRAINT "field_values_contact_id_contacts_id_fk" FOREIGN KEY ("contact_id") REFERENCES "public"."contacts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "field_values" ADD CONSTRAINT "field_values_demand_deal_id_demand_deals_id_fk" FOREIGN KEY ("demand_deal_id") REFERENCES "public"."demand_deals"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "field_values" ADD CONSTRAINT "field_values_supply_deal_id_supply_deals_id_fk" FOREIGN KEY ("supply_deal_id") REFERENCES "public"."supply_deals"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "field_values" ADD CONSTRAINT "field_values_option_id_field_options_id_fk" FOREIGN KEY ("option_id") REFERENCES "public"."field_options"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "field_values" ADD CONSTRAINT "field_values_user_value_users_id_fk" FOREIGN KEY ("user_value") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
CREATE UNIQUE INDEX "api_keys_key_hash_key" ON "api_keys" USING btree ("key_hash");--> statement-breakpoint
CREATE INDEX "api_keys_user_idx" ON "api_keys" USING btree ("user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "invites_code_hash_key" ON "invites" USING btree ("code_hash");--> statement-breakpoint
CREATE INDEX "invites_email_idx" ON "invites" USING btree ("email");--> statement-breakpoint
CREATE INDEX "team_memberships_team_idx" ON "team_memberships" USING btree ("team");--> statement-breakpoint
CREATE UNIQUE INDEX "users_auth_subject_key" ON "users" USING btree ("auth_subject");--> statement-breakpoint
CREATE UNIQUE INDEX "users_email_key" ON "users" USING btree ("email");--> statement-breakpoint
CREATE UNIQUE INDEX "users_handle_key" ON "users" USING btree ("handle");--> statement-breakpoint
CREATE UNIQUE INDEX "accounts_domain_key" ON "accounts" USING btree ("domain");--> statement-breakpoint
CREATE INDEX "accounts_side_idx" ON "accounts" USING btree ("side");--> statement-breakpoint
CREATE INDEX "accounts_owner_idx" ON "accounts" USING btree ("owner_user_id");--> statement-breakpoint
CREATE INDEX "accounts_name_idx" ON "accounts" USING btree ("name");--> statement-breakpoint
CREATE INDEX "activities_account_idx" ON "activities" USING btree ("account_id","occurred_at");--> statement-breakpoint
CREATE INDEX "activities_contact_idx" ON "activities" USING btree ("contact_id","occurred_at");--> statement-breakpoint
CREATE INDEX "activities_demand_deal_idx" ON "activities" USING btree ("demand_deal_id","occurred_at");--> statement-breakpoint
CREATE INDEX "activities_supply_deal_idx" ON "activities" USING btree ("supply_deal_id","occurred_at");--> statement-breakpoint
CREATE UNIQUE INDEX "activities_external_id_key" ON "activities" USING btree ("external_id");--> statement-breakpoint
CREATE UNIQUE INDEX "channel_links_platform_channel_key" ON "channel_links" USING btree ("platform","channel_id");--> statement-breakpoint
CREATE INDEX "channel_links_account_idx" ON "channel_links" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "contacts_account_idx" ON "contacts" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "contacts_email_idx" ON "contacts" USING btree ("email");--> statement-breakpoint
CREATE INDEX "contacts_name_idx" ON "contacts" USING btree ("full_name");--> statement-breakpoint
CREATE INDEX "contacts_confidence_idx" ON "contacts" USING btree ("confidence");--> statement-breakpoint
CREATE INDEX "capacity_commitments_account_idx" ON "capacity_commitments" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "capacity_commitments_gpu_type_idx" ON "capacity_commitments" USING btree ("gpu_type");--> statement-breakpoint
CREATE INDEX "capacity_commitments_window_idx" ON "capacity_commitments" USING btree ("starts_at","ends_at");--> statement-breakpoint
CREATE UNIQUE INDEX "inventory_listings_external_key" ON "inventory_listings" USING btree ("external_cloud_id","gpu_type","socket","gpu_count","security_tier");--> statement-breakpoint
CREATE INDEX "inventory_listings_gpu_type_idx" ON "inventory_listings" USING btree ("gpu_type");--> statement-breakpoint
CREATE INDEX "inventory_listings_stock_idx" ON "inventory_listings" USING btree ("stock_status");--> statement-breakpoint
CREATE INDEX "inventory_listings_interconnect_idx" ON "inventory_listings" USING btree ("interconnect_type");--> statement-breakpoint
CREATE INDEX "inventory_listings_account_idx" ON "inventory_listings" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "inventory_listings_observed_idx" ON "inventory_listings" USING btree ("observed_at");--> statement-breakpoint
CREATE INDEX "sites_account_idx" ON "sites" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "sites_country_idx" ON "sites" USING btree ("country_code");--> statement-breakpoint
CREATE INDEX "supply_deals_account_idx" ON "supply_deals" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "supply_deals_stage_idx" ON "supply_deals" USING btree ("stage");--> statement-breakpoint
CREATE INDEX "supply_deals_owner_idx" ON "supply_deals" USING btree ("owner_user_id");--> statement-breakpoint
CREATE INDEX "capacity_requests_deal_idx" ON "capacity_requests" USING btree ("demand_deal_id");--> statement-breakpoint
CREATE INDEX "capacity_requests_gpu_type_idx" ON "capacity_requests" USING btree ("gpu_type");--> statement-breakpoint
CREATE INDEX "capacity_requests_window_idx" ON "capacity_requests" USING btree ("starts_at","ends_at");--> statement-breakpoint
CREATE INDEX "deal_contacts_deal_idx" ON "deal_contacts" USING btree ("demand_deal_id");--> statement-breakpoint
CREATE INDEX "deal_contacts_contact_idx" ON "deal_contacts" USING btree ("contact_id");--> statement-breakpoint
CREATE INDEX "demand_deals_account_idx" ON "demand_deals" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "demand_deals_stage_idx" ON "demand_deals" USING btree ("stage");--> statement-breakpoint
CREATE INDEX "demand_deals_owner_idx" ON "demand_deals" USING btree ("owner_user_id");--> statement-breakpoint
CREATE INDEX "demand_deals_close_date_idx" ON "demand_deals" USING btree ("expected_close_date");--> statement-breakpoint
CREATE INDEX "allocations_commitment_idx" ON "allocations" USING btree ("capacity_commitment_id");--> statement-breakpoint
CREATE INDEX "allocations_deal_idx" ON "allocations" USING btree ("demand_deal_id");--> statement-breakpoint
CREATE INDEX "allocations_status_idx" ON "allocations" USING btree ("status");--> statement-breakpoint
CREATE INDEX "allocations_window_idx" ON "allocations" USING btree ("starts_at","ends_at");--> statement-breakpoint
CREATE INDEX "allocations_hold_expiry_idx" ON "allocations" USING btree ("hold_expires_at");--> statement-breakpoint
CREATE INDEX "contract_obligations_contract_idx" ON "contract_obligations" USING btree ("contract_id");--> statement-breakpoint
CREATE INDEX "contract_obligations_due_idx" ON "contract_obligations" USING btree ("due_at");--> statement-breakpoint
CREATE INDEX "contract_obligations_owner_idx" ON "contract_obligations" USING btree ("owner_user_id");--> statement-breakpoint
CREATE INDEX "contracts_account_idx" ON "contracts" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "contracts_type_idx" ON "contracts" USING btree ("type");--> statement-breakpoint
CREATE INDEX "contracts_status_idx" ON "contracts" USING btree ("status");--> statement-breakpoint
CREATE INDEX "contracts_expiry_idx" ON "contracts" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX "contracts_demand_deal_idx" ON "contracts" USING btree ("demand_deal_id");--> statement-breakpoint
CREATE INDEX "sla_metric_targets_term_idx" ON "sla_metric_targets" USING btree ("sla_term_id");--> statement-breakpoint
CREATE INDEX "sla_terms_contract_idx" ON "sla_terms" USING btree ("contract_id");--> statement-breakpoint
CREATE INDEX "compliance_artifacts_account_idx" ON "compliance_artifacts" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "compliance_artifacts_claim_idx" ON "compliance_artifacts" USING btree ("claim");--> statement-breakpoint
CREATE INDEX "compliance_artifacts_expiry_idx" ON "compliance_artifacts" USING btree ("expires_at");--> statement-breakpoint
CREATE INDEX "compliance_decisions_allocation_idx" ON "compliance_decisions" USING btree ("allocation_id");--> statement-breakpoint
CREATE INDEX "compliance_decisions_account_idx" ON "compliance_decisions" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "compliance_decisions_decision_idx" ON "compliance_decisions" USING btree ("decision");--> statement-breakpoint
CREATE INDEX "export_authorizations_account_idx" ON "export_authorizations" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "export_authorizations_expiry_idx" ON "export_authorizations" USING btree ("expires_at");--> statement-breakpoint
CREATE UNIQUE INDEX "agent_actions_idempotency_key" ON "agent_actions" USING btree ("idempotency_key");--> statement-breakpoint
CREATE INDEX "agent_actions_run_idx" ON "agent_actions" USING btree ("agent_run_id");--> statement-breakpoint
CREATE INDEX "agent_runs_task_idx" ON "agent_runs" USING btree ("agent_task_id");--> statement-breakpoint
CREATE INDEX "agent_runs_principal_idx" ON "agent_runs" USING btree ("principal_user_id");--> statement-breakpoint
CREATE UNIQUE INDEX "agent_tasks_pending_key" ON "agent_tasks" USING btree ("kind","subject") WHERE "agent_tasks"."finished_at" IS NULL;--> statement-breakpoint
CREATE INDEX "agent_tasks_claimable_idx" ON "agent_tasks" USING btree ("due_at","priority");--> statement-breakpoint
CREATE INDEX "agent_tasks_lease_idx" ON "agent_tasks" USING btree ("leased_until");--> statement-breakpoint
CREATE INDEX "facts_account_idx" ON "facts" USING btree ("account_id");--> statement-breakpoint
CREATE INDEX "facts_contact_idx" ON "facts" USING btree ("contact_id");--> statement-breakpoint
CREATE INDEX "facts_status_idx" ON "facts" USING btree ("status");--> statement-breakpoint
CREATE INDEX "facts_field_idx" ON "facts" USING btree ("field");--> statement-breakpoint
CREATE UNIQUE INDEX "field_definitions_entity_key" ON "field_definitions" USING btree ("entity","key");--> statement-breakpoint
CREATE INDEX "field_options_field_idx" ON "field_options" USING btree ("field_id");--> statement-breakpoint
CREATE UNIQUE INDEX "field_values_account_key" ON "field_values" USING btree ("field_id","account_id");--> statement-breakpoint
CREATE UNIQUE INDEX "field_values_contact_key" ON "field_values" USING btree ("field_id","contact_id");--> statement-breakpoint
CREATE UNIQUE INDEX "field_values_demand_deal_key" ON "field_values" USING btree ("field_id","demand_deal_id");--> statement-breakpoint
CREATE UNIQUE INDEX "field_values_supply_deal_key" ON "field_values" USING btree ("field_id","supply_deal_id");
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1786585246686,
"tag": "0000_initial",
"breakpoints": true
}
]
}
+28
View File
@@ -0,0 +1,28 @@
{
"name": "@pig/db",
"version": "0.1.0",
"private": true,
"license": "Apache-2.0",
"type": "module",
"main": "./src/index.ts",
"types": "./src/index.ts",
"exports": {
".": "./src/index.ts",
"./schema": "./src/schema/index.ts"
},
"scripts": {
"generate": "drizzle-kit generate",
"migrate": "tsx src/migrate.ts",
"seed": "tsx src/seed/index.ts",
"studio": "drizzle-kit studio",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@pig/core": "*",
"drizzle-orm": "^0.38.3",
"postgres": "^3.4.5"
},
"devDependencies": {
"drizzle-kit": "^0.30.1"
}
}
+37
View File
@@ -0,0 +1,37 @@
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema/index';
export type Database = ReturnType<typeof createDatabase>;
export interface DatabaseOptions {
url?: string;
/** Connection pool size. Keep modest; the agent worker opens its own. */
max?: number;
/** Log every statement. Never enable in production — values include PII. */
debug?: boolean;
}
export function createDatabase(options: DatabaseOptions = {}) {
const url = options.url ?? process.env.DATABASE_URL;
if (!url) {
throw new Error(
'DATABASE_URL is not set. PIG needs its own dedicated Postgres database — ' +
'do not point it at one shared with another application.',
);
}
const sql = postgres(url, {
max: options.max ?? 10,
// Drizzle handles its own type parsing; leaving this on causes surprises
// with numeric columns, which here carry money and GPU-hours.
transform: undefined,
onnotice: () => {},
debug: options.debug ? (_c, query) => console.debug('[db]', query) : undefined,
});
return drizzle(sql, { schema });
}
export { schema };
export * from './schema/index';
+1
View File
@@ -0,0 +1 @@
export * from './client';
+26
View File
@@ -0,0 +1,26 @@
/**
* Apply pending migrations. Safe to run repeatedly; Drizzle tracks what has
* already been applied.
*/
import { drizzle } from 'drizzle-orm/postgres-js';
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import postgres from 'postgres';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const url = process.env.DATABASE_URL;
if (!url) {
console.error('DATABASE_URL is not set.');
process.exit(1);
}
const migrationsFolder = join(dirname(fileURLToPath(import.meta.url)), '..', 'migrations');
// max: 1 — migrations must run on a single connection, in order.
const sql = postgres(url, { max: 1 });
try {
await migrate(drizzle(sql), { migrationsFolder });
console.log('Migrations applied.');
} finally {
await sql.end();
}
+231
View File
@@ -0,0 +1,231 @@
/**
* The agent layer.
*
* Three ideas here, all of which exist to make an agent safe to point at a
* database that people make commercial decisions from.
*
* 1. **The API never calls the agent.** It writes a row to `agentTasks`. A
* worker leases rows and drains them. The queue therefore survives the agent
* being offline, a restart replays nothing that already finished, and no
* request thread is ever blocked on a model.
*
* 2. **Every derived claim carries its evidence.** Enrichment writes to `facts`
* with a score, a band, a source URL and a status — not directly to the
* record. Strong signals apply themselves; weak ones queue for a human. An
* agent permitted to write unattributed claims will eventually write a wrong
* one, and nobody will be able to tell which.
*
* 3. **Every outward action is idempotent.** `agentActions` carries a unique
* idempotency key, so a retried task cannot send the same message twice.
*
* The pattern is adapted from Comp AI CRM (MIT); see NOTICE.
*/
import {
index,
integer,
jsonb,
numeric,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from 'drizzle-orm/pg-core';
import { sql } from 'drizzle-orm';
import {
agentTaskKindEnum,
agentTaskOutcomeEnum,
factBandEnum,
factStatusEnum,
} from './enums';
import { accounts, contacts } from './crm';
import { users } from './identity';
/**
* The work queue. Leased rather than locked: a worker claims a row by stamping
* `leasedUntil` into the future, and a crashed worker's rows become claimable
* again when the lease lapses. No dead-letter babysitting, no stuck jobs.
*/
export const agentTasks = pgTable(
'agent_tasks',
{
id: uuid('id').primaryKey().defaultRandom(),
kind: agentTaskKindEnum('kind').notNull(),
/**
* What the task is about — an account id, a contact id, a commitment id.
* Free text because the referent varies by kind, and a foreign key per kind
* would mean a column per kind.
*/
subject: text('subject').notNull(),
/** Why this was queued. Shown to the user; keeps agent work explicable. */
reason: text('reason'),
payload: jsonb('payload').$type<Record<string, unknown>>(),
/** Higher runs first. */
priority: integer('priority').notNull().default(0),
/** Model calls this task may spend before giving up. */
budget: integer('budget').notNull().default(4),
attempts: integer('attempts').notNull().default(0),
maxAttempts: integer('max_attempts').notNull().default(3),
/** Not eligible to run before this time. Used for backoff and scheduling. */
dueAt: timestamp('due_at', { withTimezone: true }).notNull().defaultNow(),
/** Claimed until. Null means unclaimed; past means the claim has lapsed. */
leasedUntil: timestamp('leased_until', { withTimezone: true }),
leasedBy: text('leased_by'),
startedAt: timestamp('started_at', { withTimezone: true }),
finishedAt: timestamp('finished_at', { withTimezone: true }),
outcome: agentTaskOutcomeEnum('outcome'),
error: text('error'),
requestedByUserId: uuid('requested_by_user_id').references(() => users.id, {
onDelete: 'set null',
}),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
/**
* At most one *outstanding* task per (kind, subject).
*
* Partial on `finished_at IS NULL`, so re-enriching an account next month is
* fine while queueing the same enrichment twice today is not. Without this,
* a UI that queues work on page view will melt the agent.
*/
uniqueIndex('agent_tasks_pending_key')
.on(t.kind, t.subject)
.where(sql`${t.finishedAt} IS NULL`),
index('agent_tasks_claimable_idx').on(t.dueAt, t.priority),
index('agent_tasks_lease_idx').on(t.leasedUntil),
],
);
/**
* Evidence-bearing claims produced by the agent.
*
* A fact never overwrites a record directly. It records what was claimed, about
* which field, with what confidence, on what evidence — and only then, if the
* band is `verified`, is it applied. Everything weaker waits for a person.
*/
export const facts = pgTable(
'facts',
{
id: uuid('id').primaryKey().defaultRandom(),
accountId: uuid('account_id').references(() => accounts.id, { onDelete: 'cascade' }),
contactId: uuid('contact_id').references(() => contacts.id, { onDelete: 'cascade' }),
/** The field being claimed about, e.g. `title`, `supplierType`, `gpuCount`. */
field: text('field').notNull(),
value: text('value').notNull(),
/** 01. Mapped to a band by `bandForScore` in @pig/core. */
score: numeric('score', { precision: 4, scale: 3 }).notNull(),
band: factBandEnum('band').notNull(),
status: factStatusEnum('status').notNull().default('proposed'),
/** What the claim rests on: quotes, URLs, the reasoning that produced it. */
evidence: jsonb('evidence').$type<Record<string, unknown>>(),
sourceUrl: text('source_url'),
/** How it was derived — 'web_search', 'prime_api', 'inference'. */
method: text('method'),
/** The agent run that produced this, for tracing back. */
agentRunId: uuid('agent_run_id'),
decidedByUserId: uuid('decided_by_user_id').references(() => users.id, {
onDelete: 'set null',
}),
decidedAt: timestamp('decided_at', { withTimezone: true }),
observedAt: timestamp('observed_at', { withTimezone: true }).notNull().defaultNow(),
supersededAt: timestamp('superseded_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('facts_account_idx').on(t.accountId),
index('facts_contact_idx').on(t.contactId),
index('facts_status_idx').on(t.status),
index('facts_field_idx').on(t.field),
],
);
/** One execution of the agent, for cost accounting and debugging. */
export const agentRuns = pgTable(
'agent_runs',
{
id: uuid('id').primaryKey().defaultRandom(),
agentTaskId: uuid('agent_task_id').references(() => agentTasks.id, {
onDelete: 'set null',
}),
/** Which agent — 'piggy', or a user's own connected client. */
agent: text('agent').notNull().default('piggy'),
/** The person on whose behalf this ran. */
principalUserId: uuid('principal_user_id').references(() => users.id, {
onDelete: 'set null',
}),
status: text('status').notNull().default('running'),
model: text('model'),
inputTokens: integer('input_tokens'),
outputTokens: integer('output_tokens'),
costMicroCents: integer('cost_micro_cents'),
input: jsonb('input').$type<Record<string, unknown>>(),
result: jsonb('result').$type<Record<string, unknown>>(),
summary: text('summary'),
error: text('error'),
startedAt: timestamp('started_at', { withTimezone: true }).notNull().defaultNow(),
finishedAt: timestamp('finished_at', { withTimezone: true }),
},
(t) => [
index('agent_runs_task_idx').on(t.agentTaskId),
index('agent_runs_principal_idx').on(t.principalUserId),
],
);
/**
* Side effects the agent performed. Every row that touches the world outside
* PIG lands here first, keyed by an idempotency key, so a retry is a no-op
* rather than a second message to a customer.
*/
export const agentActions = pgTable(
'agent_actions',
{
id: uuid('id').primaryKey().defaultRandom(),
agentRunId: uuid('agent_run_id').references(() => agentRuns.id, { onDelete: 'cascade' }),
/** 'update_record' | 'send_slack' | 'post_buzz' | 'create_task' */
type: text('type').notNull(),
targetType: text('target_type'),
targetId: text('target_id'),
summary: text('summary'),
/**
* Unique. This is the whole safety mechanism: an action is attempted at
* most once, no matter how many times its task is retried.
*/
idempotencyKey: text('idempotency_key').notNull(),
status: text('status').notNull().default('pending'),
externalId: text('external_id'),
error: text('error'),
metadata: jsonb('metadata').$type<Record<string, unknown>>(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('agent_actions_idempotency_key').on(t.idempotencyKey),
index('agent_actions_run_idx').on(t.agentRunId),
],
);
export type AgentTask = typeof agentTasks.$inferSelect;
export type NewAgentTask = typeof agentTasks.$inferInsert;
export type Fact = typeof facts.$inferSelect;
export type NewFact = typeof facts.$inferInsert;
export type AgentRun = typeof agentRuns.$inferSelect;
+194
View File
@@ -0,0 +1,194 @@
/**
* Allocations — the margin ledger.
*
* This is the table PIG exists for. Everything else is scaffolding around it.
*
* A `capacity_commitment` is a block of GPU-hours bought from a provider at a
* known cost. A `demand_deal` is an agreement to sell compute to a customer at
* a known price. An allocation records that some of a specific block was sold
* to a specific customer, at a specific price, for a specific window.
*
* From that single join, everything the business runs on falls out:
*
* margin = Σ(allocated hours × price) (committed hours × cost)
* utilisation = Σ(allocated hours) ÷ committed hours
* idle capacity = committed hours Σ(allocated hours)
*
* No generic CRM can compute these, because none of them has a concept of a
* cost-bearing commitment sitting behind the pipeline. That is the entire
* argument for building this rather than configuring HubSpot.
*
* Note the deliberate asymmetry in the margin formula: cost is charged against
* the FULL commitment, not merely the hours that sold. Unsold hours on a
* commitment are already paid for. Charging only the allocated share would
* report a healthy margin on a block that is bleeding money, which is precisely
* the failure this system is meant to make impossible.
*/
import {
index,
integer,
numeric,
pgTable,
text,
timestamp,
uuid,
} from 'drizzle-orm/pg-core';
import { capacityCommitments } from './supply';
import { demandDeals } from './demand';
import { users } from './identity';
export const allocations = pgTable(
'allocations',
{
id: uuid('id').primaryKey().defaultRandom(),
/** The block being drawn from. Restricted: never orphan a cost record. */
capacityCommitmentId: uuid('capacity_commitment_id')
.notNull()
.references(() => capacityCommitments.id, { onDelete: 'restrict' }),
/**
* Who it was sold to. Nullable, because internal research consumption is a
* real and important allocation with no deal and no revenue behind it.
* Leaving research burn out of the ledger overstates available capacity and
* understates true cost — the two mistakes this table prevents.
*/
demandDealId: uuid('demand_deal_id').references(() => demandDeals.id, {
onDelete: 'set null',
}),
/**
* Set when the consumer is an internal team rather than a customer.
* Mutually exclusive with `demandDealId` in practice; enforced in the
* service layer rather than by constraint, because a research allocation
* occasionally converts into a customer one and the transition should not
* require deleting the row and losing its history.
*/
internalTeam: text('internal_team'),
/** Hours drawn from the block. */
gpuHours: numeric('gpu_hours', { precision: 16, scale: 2 }).notNull(),
/**
* Sell price in cents per GPU-hour. Zero for internal research
* consumption — which is meaningful, not missing: the hours cost real money
* and earn none.
*/
pricePerGpuHourCents: integer('price_per_gpu_hour_cents').notNull().default(0),
currency: text('currency').notNull().default('USD'),
/**
* The window this allocation occupies. Must sit inside the commitment's own
* window; capacity cannot be sold before it exists or after it lapses.
*/
startsAt: timestamp('starts_at', { withTimezone: true }).notNull(),
endsAt: timestamp('ends_at', { withTimezone: true }).notNull(),
/**
* `planned` — pencilled in against a deal that has not closed
* `committed` — contractually promised to the customer
* `active` — running now
* `completed` — finished and billable
* `released` — given back; the hours return to available inventory
*
* Only `committed`, `active` and `completed` consume capacity. `planned`
* allocations are shown separately so a seller can see what the pipeline
* would do to utilisation without letting forecasts pollute the actuals.
*/
status: text('status').notNull().default('planned'),
/**
* A capacity HOLD with an expiry.
*
* When a seller reserves inventory against a deal that has not closed,
* that capacity must disappear from everyone else's availability
* immediately — otherwise two sellers promise the same GPUs and one of
* them is wrong. Holds expire on a timer so that a stalled deal releases
* inventory automatically rather than stranding it indefinitely.
*
* This single pair of fields is the clearest thing a generic CRM cannot
* do: it will happily let you create an opportunity for any amount, and
* nothing anywhere checks whether you can deliver it.
*/
holdExpiresAt: timestamp('hold_expires_at', { withTimezone: true }),
/** What else we turned away to keep this hold, in cents. Makes holds honest. */
holdOpportunityCostCents: integer('hold_opportunity_cost_cents'),
/**
* Priority ladder, borrowed from how ad servers arbitrate guaranteed
* against opportunistic demand — the closest published analogue to
* arbitrating reserved against spot against internal burn.
*
* `guaranteed` Contractually reserved; displaces everything below
* `committed` Reserved share, but not a fixed block
* `on_demand` Priced opportunistically
* `preemptible` Spot; yields to anything above it
* `internal` Research burn; the first thing displaced
*
* Lower `priority` integers win. Making this explicit is what lets the
* system answer "can I actually sell this?" rather than "is something
* technically free?"
*/
guaranteeType: text('guarantee_type').notNull().default('committed'),
priority: integer('priority').notNull().default(100),
/**
* The compliance gate. An allocation is a specific buyer against specific
* capacity in a specific jurisdiction, which is exactly the granularity at
* which export control actually applies — see compliance.ts. Null means
* unevaluated, which the service layer treats as blocking rather than
* permissive for any cross-border match.
*/
complianceDecisionId: uuid('compliance_decision_id'),
createdByUserId: uuid('created_by_user_id').references(() => users.id, {
onDelete: 'set null',
}),
notes: text('notes'),
releasedAt: timestamp('released_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('allocations_commitment_idx').on(t.capacityCommitmentId),
index('allocations_deal_idx').on(t.demandDealId),
index('allocations_status_idx').on(t.status),
index('allocations_window_idx').on(t.startsAt, t.endsAt),
/** Sweeping expired holds back into available inventory. */
index('allocations_hold_expiry_idx').on(t.holdExpiresAt),
],
);
/**
* Statuses that actually consume committed capacity.
*
* Kept here beside the column it describes so the definition cannot drift away
* from the schema. Utilisation and idle-capacity figures filter on this;
* including `planned` would let optimistic forecasting hide idle hardware.
*/
export const CONSUMING_ALLOCATION_STATUSES = ['committed', 'active', 'completed'] as const;
/**
* Statuses that block the capacity from being sold to somebody else.
*
* Deliberately WIDER than the set that counts toward utilisation and margin.
* A live hold must remove inventory from availability — otherwise two sellers
* promise the same GPUs — while not yet counting as sold, because it has not
* been. Conflating "cannot be offered to anyone else" with "earning revenue"
* is how a pipeline of optimistic holds comes to look like a full book.
*/
export const RESERVING_ALLOCATION_STATUSES = [
'planned',
...CONSUMING_ALLOCATION_STATUSES,
] as const;
export const ALLOCATION_STATUSES = [
'planned',
...CONSUMING_ALLOCATION_STATUSES,
'released',
] as const;
export type AllocationStatus = (typeof ALLOCATION_STATUSES)[number];
export type Allocation = typeof allocations.$inferSelect;
export type NewAllocation = typeof allocations.$inferInsert;
+220
View File
@@ -0,0 +1,220 @@
/**
* Export control and compliance.
*
* This file exists because of a single regulatory fact that generic CRMs
* cannot express: **country of incorporation is not a valid key.**
*
* US export controls on advanced computing apply an *ultimate parent* test.
* A licence is required to export covered items to entities headquartered in
* — or whose ultimate parent is headquartered in — certain country groups,
* **even when the entity itself sits elsewhere**. A Singapore-registered
* subsidiary of a restricted parent is restricted. The tiered "AI Diffusion
* Rule" that briefly governed this was rescinded in May 2025, but the
* headquarters test predates it, survived it, and was reaffirmed in
* subsequent guidance. Nothing comprehensive replaced the tiers, so what
* remains is: the parent test, named-entity authorisations, and case-by-case
* licensing.
*
* Two consequences shape the schema:
*
* 1. Compliance is a **predicate on the allocation edge**, evaluated when a
* specific buyer is matched to specific capacity in a specific
* jurisdiction — not a boolean on an account. Two suppliers in the same
* country can have opposite legal status.
*
* 2. The decision must be **recorded and auditable**, and re-evaluated when
* capacity is resold or a workload migrates. A decision made at signature
* does not remain true.
*
* PIG records and surfaces these facts. It is not legal advice and does not
* make the determination for you: `decision` is set by a human or by an
* explicit rule version, and always carries its evidence.
*/
import {
boolean,
index,
jsonb,
pgTable,
text,
timestamp,
uuid,
} from 'drizzle-orm/pg-core';
import { accounts } from './crm';
import { users } from './identity';
import { allocations } from './allocations';
/**
* A named authorisation permitting business that would otherwise need a
* licence. These expire — some on the order of months — and an expired
* authorisation silently converts lawful business into unlawful business,
* so `expiresAt` is indexed and alerted on.
*/
export const exportAuthorizations = pgTable(
'export_authorizations',
{
id: uuid('id').primaryKey().defaultRandom(),
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id, { onDelete: 'cascade' }),
/**
* `none` No authorisation on file
* `licence` An individually issued export licence
* `listed_entity` Named on a published authorisation list
* `dc_veu` Validated end user / datacentre authorisation
* `case_by_case` Reviewed individually, typically with a volume cap
*/
authorizationType: text('authorization_type').notNull().default('none'),
reference: text('reference'),
scopeNotes: text('scope_notes'),
issuedAt: timestamp('issued_at', { withTimezone: true }),
expiresAt: timestamp('expires_at', { withTimezone: true }),
/** Where this was verified from. Never assert an authorisation unsourced. */
evidenceUrl: text('evidence_url'),
verifiedByUserId: uuid('verified_by_user_id').references(() => users.id, {
onDelete: 'set null',
}),
verifiedAt: timestamp('verified_at', { withTimezone: true }),
/**
* Set when the underlying rules are in flux for this counterparty, which
* they frequently are. Drives periodic re-verification rather than
* trusting a stale check.
*/
volatile: boolean('volatile').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('export_authorizations_account_idx').on(t.accountId),
index('export_authorizations_expiry_idx').on(t.expiresAt),
],
);
/**
* The blocking predicate, evaluated when capacity is matched to a buyer.
*
* Stored rather than computed on the fly so that the reasoning behind a
* decision survives the rule changing, the staff changing, and the deal being
* audited two years later. `ruleVersion` records which version of the check
* produced this outcome, so a rule change can be replayed across open
* allocations rather than quietly changing history.
*/
export const complianceDecisions = pgTable(
'compliance_decisions',
{
id: uuid('id').primaryKey().defaultRandom(),
allocationId: uuid('allocation_id').references(() => allocations.id, {
onDelete: 'cascade',
}),
/** The buying party. */
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id, { onDelete: 'cascade' }),
/**
* The three coordinates the determination actually turns on. Recorded as
* they were at decision time, because all three can change afterwards.
*/
beneficialOwnerName: text('beneficial_owner_name'),
ultimateParentCountry: text('ultimate_parent_country'),
physicalJurisdiction: text('physical_jurisdiction'),
endUse: text('end_use'),
/** `allow` | `block` | `needs_review` — blocking, never advisory. */
decision: text('decision').notNull().default('needs_review'),
rationale: text('rationale'),
ruleVersion: text('rule_version'),
decidedByUserId: uuid('decided_by_user_id').references(() => users.id, {
onDelete: 'set null',
}),
decidedAt: timestamp('decided_at', { withTimezone: true }),
/**
* What would invalidate this: `resale`, `migration`, `ownership_change`,
* `authorization_expiry`. A decision is only as good as its assumptions.
*/
reEvaluationTriggers: jsonb('re_evaluation_triggers')
.$type<string[]>()
.notNull()
.default([]),
supersededAt: timestamp('superseded_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('compliance_decisions_allocation_idx').on(t.allocationId),
index('compliance_decisions_account_idx').on(t.accountId),
index('compliance_decisions_decision_idx').on(t.decision),
],
);
/**
* Compliance evidence about a counterparty — SOC 2, ISO 27001, ISO 42001,
* penetration tests, insurance certificates.
*
* Never a boolean, for two reasons observed repeatedly in this market.
*
* First, **scope**: a SOC 2 report may cover only some of a provider's
* products, and may carve out the colocation provider as a subservice
* organisation — meaning the physical security the report appears to attest
* is explicitly not attested.
*
* Second, **"certified" versus "aligned with"**: marketing routinely places
* "aligns its security programme to SOC 2 and ISO 27001" beside genuine
* certifications. The distinction decides enterprise procurement, so it gets
* its own column rather than being lost in prose.
*/
export const complianceArtifacts = pgTable(
'compliance_artifacts',
{
id: uuid('id').primaryKey().defaultRandom(),
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id, { onDelete: 'cascade' }),
/** e.g. `soc2`, `iso27001`, `iso42001`, `pentest`, `cyber_insurance`. */
claim: text('claim').notNull(),
/** Which products or facilities the claim actually covers. */
scope: text('scope'),
/** True certification versus self-declared alignment. The whole point. */
isCertified: boolean('is_certified').notNull().default(false),
/** SOC 2 specifics, null for other artefact types. */
soc2Type: text('soc2_type'),
observationWindowStart: timestamp('observation_window_start', { withTimezone: true }),
observationWindowEnd: timestamp('observation_window_end', { withTimezone: true }),
auditFirm: text('audit_firm'),
/**
* Whether the report carves out subservice organisations. A carve-out
* means the colocation provider's controls are NOT covered.
*/
carveOutMethod: text('carve_out_method'),
productsInScope: jsonb('products_in_scope').$type<string[]>().notNull().default([]),
evidenceUrl: text('evidence_url'),
verifiedByUserId: uuid('verified_by_user_id').references(() => users.id, {
onDelete: 'set null',
}),
verifiedAt: timestamp('verified_at', { withTimezone: true }),
expiresAt: timestamp('expires_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('compliance_artifacts_account_idx').on(t.accountId),
index('compliance_artifacts_claim_idx').on(t.claim),
index('compliance_artifacts_expiry_idx').on(t.expiresAt),
],
);
export type ExportAuthorization = typeof exportAuthorizations.$inferSelect;
export type ComplianceDecision = typeof complianceDecisions.$inferSelect;
export type ComplianceArtifact = typeof complianceArtifacts.$inferSelect;
+348
View File
@@ -0,0 +1,348 @@
/**
* Contracts: MSA, DPA, SLA, order forms, capacity commitments.
*
* Two things about this market shape the design.
*
* First, contracts are not a closing formality on the demand side — legal sits
* early in the pipeline, because a customer will not hand workloads to an
* infrastructure provider before paper is in place.
*
* Second, and less obviously: **service levels are promised differently on each
* side of the business**. A compute aggregator generally cannot offer a
* conventional uptime guarantee on capacity it resells and does not control,
* and says so publicly. What it offers self-serve customers instead is a
* reliability tier plus a service-credits policy. Yet the same company
* negotiates heavyweight SLAs upstream with providers, and bespoke ones
* downstream with enterprise customers on dedicated clusters.
*
* All three shapes therefore coexist, and the schema holds them without
* pretending any one is the others. See `slaKind` below.
*/
import {
boolean,
index,
integer,
jsonb,
numeric,
pgTable,
text,
timestamp,
uuid,
} from 'drizzle-orm/pg-core';
import {
contractStatusEnum,
contractTypeEnum,
slaKindEnum,
slaMetricEnum,
} from './enums';
import { accounts } from './crm';
import { demandDeals } from './demand';
import { capacityCommitments, supplyDeals } from './supply';
import { users } from './identity';
export const contracts = pgTable(
'contracts',
{
id: uuid('id').primaryKey().defaultRandom(),
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id, { onDelete: 'cascade' }),
type: contractTypeEnum('type').notNull(),
status: contractStatusEnum('status').notNull().default('draft'),
/**
* Which side of the market this paper governs. The same account can hold
* both — we may buy capacity from a neocloud under one MSA while selling
* them managed training under another.
*/
side: text('side').notNull().default('demand'),
title: text('title').notNull(),
/** Counterparty's own reference, for reconciliation with their systems. */
externalReference: text('external_reference'),
/** Optional links to whatever this contract is about. */
demandDealId: uuid('demand_deal_id').references(() => demandDeals.id, {
onDelete: 'set null',
}),
supplyDealId: uuid('supply_deal_id').references(() => supplyDeals.id, {
onDelete: 'set null',
}),
capacityCommitmentId: uuid('capacity_commitment_id').references(
() => capacityCommitments.id,
{ onDelete: 'set null' },
),
/**
* An order form or SLA usually hangs beneath a master agreement. Modelling
* that hierarchy means "which MSA governs this order form?" is answerable,
* which is the first question asked in any dispute.
*
* Note the order of precedence in real master agreements: **the order form
* beats the MSA**, then exhibits and addenda, then the body of the
* agreement, then documentation. So an order-form-level override is not an
* annotation on the master terms — it supersedes them, and must be stored
* as data rather than left in a notes field.
*/
parentContractId: uuid('parent_contract_id'),
/**
* The signing entity, where it differs from the account.
*
* Affiliates routinely execute their own order forms under a parent's
* master agreement, binding themselves as if an original party. Assuming
* one contract equals one legal entity misfiles the counterparty on
* exactly the deals large enough to matter.
*/
contractingPartyName: text('contracting_party_name'),
/**
* Take-or-pay and prepayment terms. Together with `terminationTier` below
* these are what make a backlog figure meaningful: contracted revenue
* under a prepaid take-or-pay commitment and contracted revenue cancellable
* on ninety days' notice are not the same asset, and summing them
* unweighted overstates the book.
*/
takeOrPayFloorPct: numeric('take_or_pay_floor_pct', { precision: 6, scale: 2 }),
prepaidPct: numeric('prepaid_pct', { precision: 6, scale: 2 }),
/**
* `1_prepaid` Take-or-pay with prepayment — the highest quality
* `2_take_or_pay` Multi-year take-or-pay, no prepayment
* `3_cancellable` Monthly billing with a short termination right
*/
terminationTier: text('termination_tier'),
/**
* Whether this contract may be assigned to a substitute operator on
* default, and within how long. A negotiated step-in right can require
* assignment of the customer agreement *and the underlying datacentre
* agreement* within days — which only works if the link between the two
* exists in a system somewhere. That link is `capacityCommitmentId` above.
*/
assignableOnDefault: boolean('assignable_on_default').notNull().default(false),
assignmentDeadlineBusinessDays: integer('assignment_deadline_business_days'),
effectiveAt: timestamp('effective_at', { withTimezone: true }),
expiresAt: timestamp('expires_at', { withTimezone: true }),
executedAt: timestamp('executed_at', { withTimezone: true }),
terminatedAt: timestamp('terminated_at', { withTimezone: true }),
isAutoRenew: boolean('is_auto_renew').notNull().default(false),
/** Days of notice to prevent auto-renewal. Drives the renewal alarm. */
noticeDays: integer('notice_days'),
valueCents: integer('value_cents'),
currency: text('currency').notNull().default('USD'),
governingLaw: text('governing_law'),
/** Link to the executed document. PIG stores the pointer, not the paper. */
documentUrl: text('document_url'),
ownerUserId: uuid('owner_user_id').references(() => users.id, { onDelete: 'set null' }),
notes: text('notes'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('contracts_account_idx').on(t.accountId),
index('contracts_type_idx').on(t.type),
index('contracts_status_idx').on(t.status),
index('contracts_expiry_idx').on(t.expiresAt),
index('contracts_demand_deal_idx').on(t.demandDealId),
],
);
/**
* The service-level terms attached to a contract.
*
* `slaKind` is the important column:
*
* `none` Self-serve. No service commitment whatsoever.
* `credits_policy` The common case for resold capacity — a reliability tier
* plus service credits when a provider fails. Not an uptime
* guarantee, and must not be displayed as one.
* `negotiated` A real signed SLA with committed, measurable metrics.
*
* Recording a credits policy as though it were a negotiated SLA would let a
* seller promise a customer something the business has not agreed to underwrite.
*/
export const slaTerms = pgTable(
'sla_terms',
{
id: uuid('id').primaryKey().defaultRandom(),
contractId: uuid('contract_id')
.notNull()
.references(() => contracts.id, { onDelete: 'cascade' }),
kind: slaKindEnum('kind').notNull().default('credits_policy'),
/** Committed uptime, where one is genuinely committed. */
uptimeTargetPct: numeric('uptime_target_pct', { precision: 6, scale: 3 }),
/** Hours to replace a failed node — often more negotiated than uptime. */
nodeReplacementHours: integer('node_replacement_hours'),
mttrHours: integer('mttr_hours'),
supportResponseHours: integer('support_response_hours'),
/** 'monthly' | 'quarterly' — the window uptime is measured over. */
measurementWindow: text('measurement_window').notNull().default('monthly'),
/**
* What the target is measured against: `region`, `instance`, `node`,
* `rack`, `cluster`, `endpoint`.
*
* Rack-scale systems are commonly sold with TWO simultaneous levels — for
* example 99% per node alongside 95% per rack, where a rack counts as
* healthy at 16 of 18 nodes. Recording only one of them misstates what was
* promised, so additional levels go in `slaMetricTargets` and this column
* names the basis of the headline figure.
*/
measurementUnit: text('measurement_unit').notNull().default('cluster'),
/**
* The remedy actually available. This is the field that matters most, and
* the one a credits-only model gets wrong.
*
* `service_credit` A capped percentage of fees, claimed within a window
* `fee_abatement` Payment obligations CANCELLED for the affected
* capacity until service is restored — uncapped in
* duration, and far more valuable than a credit
* `termination_right` The customer may exit
*
* Fee abatement is real and negotiated: a leading provider's filed master
* agreement cancels fees for services not performed once a hardware
* failure persists two consecutive business days, and does not resume them
* until the failure is resolved.
*/
remedyType: text('remedy_type').notNull().default('service_credit'),
/** Consecutive duration triggering abatement, and its unit. */
abatementTriggerValue: integer('abatement_trigger_value'),
abatementTriggerUnit: text('abatement_trigger_unit'),
/**
* Deadline to file a claim, and its unit — observed across the market from
* ten days to two billing cycles. Miss it and the credit is simply
* forfeit, which makes this an operational alarm, not a footnote.
*/
claimDeadlineValue: integer('claim_deadline_value'),
claimDeadlineUnit: text('claim_deadline_unit').notNull().default('days'),
/** Credits commonly expire if unused. */
creditExpiryMonths: integer('credit_expiry_months'),
/** Whether credits are the sole and exclusive remedy. Usually yes. */
isSoleRemedy: boolean('is_sole_remedy').notNull().default(true),
/**
* Contractual spare-pool obligation — that the provider keeps enough
* spares on hand to replace what cannot be remediated. Observed scoped to
* **both compute nodes and network switches, per location**; switches are
* routinely forgotten and are just as capable of stranding a cluster.
*/
sparePoolObligation: text('spare_pool_obligation'),
sparePoolScope: jsonb('spare_pool_scope').$type<string[]>().notNull().default([]),
/**
* Maintenance classes and their notice periods — planned, planned-critical
* and emergency are typically distinct, with different notice and
* different treatment in the uptime calculation. Some allowance of
* maintenance hours is normally excluded from the measurement entirely.
*/
maintenanceClasses: jsonb('maintenance_classes')
.$type<
{
class: string;
noticeValue: number;
noticeUnit: string;
allowancePerPeriodHours?: number;
excludedFromUptime: boolean;
}[]
>()
.notNull()
.default([]),
/**
* Days per year during which only "reasonable endeavours" apply and the
* service levels are suspended. A real, negotiated carve-out that can span
* months, and one that silently voids the guarantee being sold.
*/
reasonableEndeavoursDaysPerYear: integer('reasonable_endeavours_days_per_year'),
/** Hours within which a root cause analysis is owed after a major incident. */
rcaDeliveryHours: integer('rca_delivery_hours'),
/**
* Credit schedule: bands of missed target mapped to the percentage of fees
* credited back. Stored as JSON because every counterparty structures these
* differently and normalising them loses the shape of the deal.
*/
creditSchedule: jsonb('credit_schedule')
.$type<{ belowPct: number; creditPct: number }[]>()
.notNull()
.default([]),
/** Maximum credits payable in a window, as a share of fees. */
creditCapPct: numeric('credit_cap_pct', { precision: 6, scale: 3 }),
/** Carve-outs: maintenance windows, force majeure, customer-caused faults. */
exclusions: text('exclusions'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index('sla_terms_contract_idx').on(t.contractId)],
);
/** An individual committed metric, where an SLA commits several. */
export const slaMetricTargets = pgTable(
'sla_metric_targets',
{
id: uuid('id').primaryKey().defaultRandom(),
slaTermId: uuid('sla_term_id')
.notNull()
.references(() => slaTerms.id, { onDelete: 'cascade' }),
metric: slaMetricEnum('metric').notNull(),
targetValue: numeric('target_value', { precision: 12, scale: 3 }).notNull(),
unit: text('unit'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [index('sla_metric_targets_term_idx').on(t.slaTermId)],
);
/**
* Dated obligations arising from a contract — renewal notice deadlines,
* milestone deliverables, true-up dates, security reviews.
*
* These are what actually get missed. A contract row with an expiry date is
* inert; an obligation with a due date can be alerted on, which is the point.
*/
export const contractObligations = pgTable(
'contract_obligations',
{
id: uuid('id').primaryKey().defaultRandom(),
contractId: uuid('contract_id')
.notNull()
.references(() => contracts.id, { onDelete: 'cascade' }),
title: text('title').notNull(),
description: text('description'),
/** 'renewal_notice' | 'milestone' | 'payment' | 'review' | 'true_up' */
kind: text('kind').notNull().default('milestone'),
dueAt: timestamp('due_at', { withTimezone: true }).notNull(),
completedAt: timestamp('completed_at', { withTimezone: true }),
ownerUserId: uuid('owner_user_id').references(() => users.id, { onDelete: 'set null' }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('contract_obligations_contract_idx').on(t.contractId),
index('contract_obligations_due_idx').on(t.dueAt),
index('contract_obligations_owner_idx').on(t.ownerUserId),
],
);
export type Contract = typeof contracts.$inferSelect;
export type NewContract = typeof contracts.$inferInsert;
export type SlaTerm = typeof slaTerms.$inferSelect;
export type ContractObligation = typeof contractObligations.$inferSelect;
+244
View File
@@ -0,0 +1,244 @@
/**
* The ordinary CRM core: accounts, contacts, activities.
*
* Deliberately unremarkable. The compute-specific intelligence lives in
* `supply.ts`, `demand.ts` and `allocations.ts`; this file exists so that those
* have something conventional to hang from, and so importing from an existing
* CRM is a straight mapping.
*/
import {
boolean,
index,
integer,
jsonb,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from 'drizzle-orm/pg-core';
import {
accountSideEnum,
activityTypeEnum,
affiliationKindEnum,
confidenceGradeEnum,
customerSegmentEnum,
recordSourceEnum,
supplierTypeEnum,
} from './enums';
import { users } from './identity';
export const accounts = pgTable(
'accounts',
{
id: uuid('id').primaryKey().defaultRandom(),
name: text('name').notNull(),
/** Primary key for deduplication in practice; more reliable than name. */
domain: text('domain'),
website: text('website'),
description: text('description'),
logoUrl: text('logo_url'),
/**
* Which side of the market this account sits on. `both` is common and
* correct — a neocloud may sell capacity to us and buy managed training
* from us in the same quarter.
*/
side: accountSideEnum('side').notNull().default('demand'),
supplierType: supplierTypeEnum('supplier_type'),
customerSegment: customerSegmentEnum('customer_segment'),
/** Headquarters. Distinct from where their capacity physically sits. */
country: text('country'),
region: text('region'),
/**
* Legal jurisdiction governing the relationship. Export controls make this
* a commercial fact rather than an administrative one — some capacity
* cannot lawfully serve some customers.
*/
jurisdiction: text('jurisdiction'),
/**
* Corporate ownership — and specifically the ULTIMATE parent.
*
* US export controls on advanced computing apply a headquarters test that
* reaches through the corporate tree: an entity may be restricted because
* of where its ultimate parent sits, **even though the entity itself is
* located somewhere unrestricted**. So the country on this record is not
* sufficient to determine whether a sale is lawful, and a CRM that stores
* only `country` cannot answer the question at all.
*
* See compliance.ts, where the determination is made per allocation.
*/
ultimateParentAccountId: uuid('ultimate_parent_account_id'),
ultimateParentName: text('ultimate_parent_name'),
ultimateParentCountry: text('ultimate_parent_country'),
/** When ownership was last actually verified, rather than assumed. */
ownershipVerifiedAt: timestamp('ownership_verified_at', { withTimezone: true }),
linkedinUrl: text('linkedin_url'),
twitterUrl: text('twitter_url'),
githubOrg: text('github_org'),
ownerUserId: uuid('owner_user_id').references(() => users.id, { onDelete: 'set null' }),
source: recordSourceEnum('source').notNull().default('manual'),
/** Where a seeded or enriched claim came from. Shown in the UI. */
sourceUrl: text('source_url'),
confidence: confidenceGradeEnum('confidence').notNull().default('confirmed'),
lastActivityAt: timestamp('last_activity_at', { withTimezone: true }),
archivedAt: timestamp('archived_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('accounts_domain_key').on(t.domain),
index('accounts_side_idx').on(t.side),
index('accounts_owner_idx').on(t.ownerUserId),
index('accounts_name_idx').on(t.name),
],
);
export const contacts = pgTable(
'contacts',
{
id: uuid('id').primaryKey().defaultRandom(),
accountId: uuid('account_id').references(() => accounts.id, { onDelete: 'set null' }),
fullName: text('full_name').notNull(),
firstName: text('first_name'),
lastName: text('last_name'),
title: text('title'),
/**
* Nullable and left null far more often than a generic CRM would expect.
* PIG never infers an address from a name and a domain: a guessed address
* is both unreliable and, when it reaches a real person, rude.
*/
email: text('email'),
phone: text('phone'),
linkedinUrl: text('linkedin_url'),
twitterHandle: text('twitter_handle'),
githubHandle: text('github_handle'),
websiteUrl: text('website_url'),
avatarUrl: text('avatar_url'),
/**
* How this person relates to the account. Being named in a company's
* repository or on its papers does not make someone an employee, and a CRM
* that conflates authorship with employment will embarrass whoever acts on
* it.
*/
affiliation: affiliationKindEnum('affiliation').notNull().default('unknown'),
/** Set when someone has demonstrably moved on. Keeps the record honest. */
departedAt: timestamp('departed_at', { withTimezone: true }),
/** Whether this person can decide, influence, or merely inform a deal. */
isDecisionMaker: boolean('is_decision_maker').notNull().default(false),
ownerUserId: uuid('owner_user_id').references(() => users.id, { onDelete: 'set null' }),
source: recordSourceEnum('source').notNull().default('manual'),
sourceUrl: text('source_url'),
confidence: confidenceGradeEnum('confidence').notNull().default('confirmed'),
/** Free-text note on provenance, shown beside low-confidence records. */
confidenceNote: text('confidence_note'),
lastActivityAt: timestamp('last_activity_at', { withTimezone: true }),
archivedAt: timestamp('archived_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('contacts_account_idx').on(t.accountId),
index('contacts_email_idx').on(t.email),
index('contacts_name_idx').on(t.fullName),
index('contacts_confidence_idx').on(t.confidence),
],
);
/**
* The activity stream. Polymorphic by nullable foreign key rather than a
* generic subject table: there are only four possible parents, Postgres can
* enforce all four, and the query planner handles the partial indexes well.
*/
export const activities = pgTable(
'activities',
{
id: uuid('id').primaryKey().defaultRandom(),
type: activityTypeEnum('type').notNull(),
subject: text('subject'),
body: text('body'),
accountId: uuid('account_id').references(() => accounts.id, { onDelete: 'cascade' }),
contactId: uuid('contact_id').references(() => contacts.id, { onDelete: 'cascade' }),
/** Set for demand-side deals; see demand.ts. */
demandDealId: uuid('demand_deal_id'),
/** Set for supply-side engagements; see supply.ts. */
supplyDealId: uuid('supply_deal_id'),
/** Null when the actor was Piggy rather than a person. */
actorUserId: uuid('actor_user_id').references(() => users.id, { onDelete: 'set null' }),
/** Set when an agent produced this entry, naming which one. */
actorAgent: text('actor_agent'),
occurredAt: timestamp('occurred_at', { withTimezone: true }).notNull().defaultNow(),
/** Structured payload — stage transitions, Slack permalinks, and so on. */
meta: jsonb('meta').$type<Record<string, unknown>>(),
/** External identity, for idempotent sync from Slack, Buzz or email. */
externalId: text('external_id'),
source: recordSourceEnum('source').notNull().default('manual'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('activities_account_idx').on(t.accountId, t.occurredAt),
index('activities_contact_idx').on(t.contactId, t.occurredAt),
index('activities_demand_deal_idx').on(t.demandDealId, t.occurredAt),
index('activities_supply_deal_idx').on(t.supplyDealId, t.occurredAt),
uniqueIndex('activities_external_id_key').on(t.externalId),
],
);
/**
* Communication channels linked to an account — a Slack channel, a Buzz room.
* This is what lets a deal alert land where the deal is actually discussed
* rather than in a generic firehose nobody reads.
*/
export const channelLinks = pgTable(
'channel_links',
{
id: uuid('id').primaryKey().defaultRandom(),
/** 'slack' | 'buzz' — kept as text so a new platform needs no migration. */
platform: text('platform').notNull(),
channelId: text('channel_id').notNull(),
channelName: text('channel_name'),
accountId: uuid('account_id').references(() => accounts.id, { onDelete: 'cascade' }),
/** Notify on stage changes, idle-capacity alerts, renewals due. */
notifyOn: jsonb('notify_on').$type<string[]>().notNull().default([]),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('channel_links_platform_channel_key').on(t.platform, t.channelId),
index('channel_links_account_idx').on(t.accountId),
],
);
/** Denormalised counter, kept for cheap list rendering. */
export const accountStats = pgTable('account_stats', {
accountId: uuid('account_id')
.primaryKey()
.references(() => accounts.id, { onDelete: 'cascade' }),
openDeals: integer('open_deals').notNull().default(0),
contactCount: integer('contact_count').notNull().default(0),
refreshedAt: timestamp('refreshed_at', { withTimezone: true }).notNull().defaultNow(),
});
export type Account = typeof accounts.$inferSelect;
export type NewAccount = typeof accounts.$inferInsert;
export type Contact = typeof contacts.$inferSelect;
export type NewContact = typeof contacts.$inferInsert;
export type Activity = typeof activities.$inferSelect;
+177
View File
@@ -0,0 +1,177 @@
/**
* The demand side: what customers want, and what we have sold them.
*
* The stage set follows how enterprise AI-infrastructure deals are publicly
* described as progressing, with `legal` deliberately second — see the note on
* DEMAND_STAGES in @pig/core. A `capacityRequest` is modelled separately from
* the deal because the technical shape of what a customer needs is negotiated
* on a different clock from the commercials, and because it is the thing that
* gets matched against inventory.
*/
import {
boolean,
index,
integer,
jsonb,
numeric,
pgTable,
text,
timestamp,
uuid,
} from 'drizzle-orm/pg-core';
import {
demandStageEnum,
interconnectTypeEnum,
productLineEnum,
securityTierEnum,
} from './enums';
import { accounts, contacts } from './crm';
import { users } from './identity';
export const demandDeals = pgTable(
'demand_deals',
{
id: uuid('id').primaryKey().defaultRandom(),
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
description: text('description'),
/**
* One account routinely carries several independent opportunities across
* product lines. Collapsing everything into "GPU hours" would make
* land-and-expand — the actual motion in this market — invisible.
*/
productLine: productLineEnum('product_line').notNull().default('compute_reserved'),
stage: demandStageEnum('stage').notNull().default('qualification'),
stageChangedAt: timestamp('stage_changed_at', { withTimezone: true }).notNull().defaultNow(),
ownerUserId: uuid('owner_user_id').references(() => users.id, { onDelete: 'set null' }),
primaryContactId: uuid('primary_contact_id').references(() => contacts.id, {
onDelete: 'set null',
}),
/** Annual contract value in cents. */
acvCents: integer('acv_cents'),
/** Total contract value across the whole term, in cents. */
tcvCents: integer('tcv_cents'),
currency: text('currency').notNull().default('USD'),
termMonths: integer('term_months'),
/** Forecast confidence, 01. Distinct from stage: late deals still die. */
probability: numeric('probability', { precision: 4, scale: 3 }),
expectedCloseDate: timestamp('expected_close_date', { withTimezone: true }),
closedAt: timestamp('closed_at', { withTimezone: true }),
closedReason: text('closed_reason'),
/**
* Whether legal is cleared. Tracked as a flag as well as a stage, because a
* deal can advance past `legal` into scoping while an amendment is still
* outstanding, and shipping capacity without executed paper is the mistake
* this field exists to prevent.
*/
msaExecuted: boolean('msa_executed').notNull().default(false),
dpaExecuted: boolean('dpa_executed').notNull().default(false),
/** Set when this deal is a renewal or expansion of an earlier one. */
parentDealId: uuid('parent_deal_id'),
lastActivityAt: timestamp('last_activity_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('demand_deals_account_idx').on(t.accountId),
index('demand_deals_stage_idx').on(t.stage),
index('demand_deals_owner_idx').on(t.ownerUserId),
index('demand_deals_close_date_idx').on(t.expectedCloseDate),
],
);
/**
* The technical shape of what a customer needs.
*
* Separated from the deal because this is the object that gets matched against
* supply. A seller asking "what have we got that fits this?" is asking about
* hardware, dates and constraints — not about ACV.
*/
export const capacityRequests = pgTable(
'capacity_requests',
{
id: uuid('id').primaryKey().defaultRandom(),
demandDealId: uuid('demand_deal_id')
.notNull()
.references(() => demandDeals.id, { onDelete: 'cascade' }),
gpuType: text('gpu_type'),
/** Acceptable substitutes, in preference order. Widens the match. */
gpuTypeAlternatives: jsonb('gpu_type_alternatives').$type<string[]>().notNull().default([]),
gpuCount: integer('gpu_count').notNull(),
/**
* A hard requirement for distributed training and irrelevant for inference.
* Getting this wrong in either direction is expensive: sell Ethernet to a
* training customer and it fails; insist on InfiniBand for an inference
* customer and you lose on price.
*/
requiresHighSpeedInterconnect: boolean('requires_high_speed_interconnect')
.notNull()
.default(false),
minInterconnectType: interconnectTypeEnum('min_interconnect_type'),
minSecurityTier: securityTierEnum('min_security_tier').notNull().default('secure_cloud'),
/** Regions the workload may run in, and those it may not. */
allowedRegions: jsonb('allowed_regions').$type<string[]>().notNull().default([]),
excludedJurisdictions: jsonb('excluded_jurisdictions').$type<string[]>().notNull().default([]),
startsAt: timestamp('starts_at', { withTimezone: true }),
endsAt: timestamp('ends_at', { withTimezone: true }),
/** Total hours wanted; drives the allocation arithmetic. */
totalGpuHours: numeric('total_gpu_hours', { precision: 16, scale: 2 }),
/** Ceiling the customer will pay, in cents. The other half of the spread. */
maxPricePerGpuHourCents: integer('max_price_per_gpu_hour_cents'),
/** Compliance requirements surfaced during procurement. */
requiredCertifications: jsonb('required_certifications').$type<string[]>().notNull().default([]),
notes: text('notes'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('capacity_requests_deal_idx').on(t.demandDealId),
index('capacity_requests_gpu_type_idx').on(t.gpuType),
index('capacity_requests_window_idx').on(t.startsAt, t.endsAt),
],
);
/** Many-to-many between deals and the people involved in them. */
export const dealContacts = pgTable(
'deal_contacts',
{
id: uuid('id').primaryKey().defaultRandom(),
demandDealId: uuid('demand_deal_id')
.notNull()
.references(() => demandDeals.id, { onDelete: 'cascade' }),
contactId: uuid('contact_id')
.notNull()
.references(() => contacts.id, { onDelete: 'cascade' }),
/** economic buyer | technical evaluator | champion | procurement | legal | blocker */
role: text('role'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('deal_contacts_deal_idx').on(t.demandDealId),
index('deal_contacts_contact_idx').on(t.contactId),
],
);
export type DemandDeal = typeof demandDeals.$inferSelect;
export type NewDemandDeal = typeof demandDeals.$inferInsert;
export type CapacityRequest = typeof capacityRequests.$inferSelect;
export type NewCapacityRequest = typeof capacityRequests.$inferInsert;
+74
View File
@@ -0,0 +1,74 @@
/**
* Postgres enum types, derived from the shared ontology in `@pig/core`.
*
* Defining them here from the same constant arrays the application uses means a
* stage can never mean one thing in the database and another in the UI. Adding
* a value is a migration, which is the correct amount of friction for changing
* the vocabulary of the business.
*/
import { pgEnum } from 'drizzle-orm/pg-core';
import {
ACCOUNT_SIDES,
ACTIVITY_TYPES,
AFFILIATION_KINDS,
AGENT_TASK_KINDS,
AGENT_TASK_OUTCOMES,
CONFIDENCE_GRADES,
CONTRACT_STATUSES,
CONTRACT_TYPES,
CUSTOMER_SEGMENTS,
DEMAND_STAGES,
FACT_BANDS,
FACT_STATUSES,
GPU_SOCKETS,
INTERCONNECT_TYPES,
PRODUCT_LINES,
RECORD_SOURCES,
SECURITY_TIERS,
SLA_KINDS,
SLA_METRICS,
STOCK_STATUSES,
SUPPLIER_TYPES,
SUPPLY_STAGES,
TEAM_ROLES,
TEAMS,
} from '@pig/core';
// Drizzle's pgEnum wants a mutable, non-empty tuple; the ontology exports
// readonly arrays. The cast is safe because every ontology constant is a
// non-empty literal array — but TypeScript cannot prove non-emptiness from the
// array type alone, hence the widening step.
const mut = <T extends readonly [string, ...string[]]>(v: T) =>
[...v] as [T[number], ...T[number][]];
export const teamEnum = pgEnum('pig_team', mut(TEAMS));
export const teamRoleEnum = pgEnum('pig_team_role', mut(TEAM_ROLES));
export const accountSideEnum = pgEnum('pig_account_side', mut(ACCOUNT_SIDES));
export const supplierTypeEnum = pgEnum('pig_supplier_type', mut(SUPPLIER_TYPES));
export const customerSegmentEnum = pgEnum('pig_customer_segment', mut(CUSTOMER_SEGMENTS));
export const productLineEnum = pgEnum('pig_product_line', mut(PRODUCT_LINES));
export const demandStageEnum = pgEnum('pig_demand_stage', mut(DEMAND_STAGES));
export const supplyStageEnum = pgEnum('pig_supply_stage', mut(SUPPLY_STAGES));
export const gpuSocketEnum = pgEnum('pig_gpu_socket', mut(GPU_SOCKETS));
export const interconnectTypeEnum = pgEnum('pig_interconnect_type', mut(INTERCONNECT_TYPES));
export const securityTierEnum = pgEnum('pig_security_tier', mut(SECURITY_TIERS));
export const stockStatusEnum = pgEnum('pig_stock_status', mut(STOCK_STATUSES));
export const contractTypeEnum = pgEnum('pig_contract_type', mut(CONTRACT_TYPES));
export const contractStatusEnum = pgEnum('pig_contract_status', mut(CONTRACT_STATUSES));
export const slaKindEnum = pgEnum('pig_sla_kind', mut(SLA_KINDS));
export const slaMetricEnum = pgEnum('pig_sla_metric', mut(SLA_METRICS));
export const factBandEnum = pgEnum('pig_fact_band', mut(FACT_BANDS));
export const factStatusEnum = pgEnum('pig_fact_status', mut(FACT_STATUSES));
export const activityTypeEnum = pgEnum('pig_activity_type', mut(ACTIVITY_TYPES));
export const agentTaskKindEnum = pgEnum('pig_agent_task_kind', mut(AGENT_TASK_KINDS));
export const agentTaskOutcomeEnum = pgEnum('pig_agent_task_outcome', mut(AGENT_TASK_OUTCOMES));
export const recordSourceEnum = pgEnum('pig_record_source', mut(RECORD_SOURCES));
export const confidenceGradeEnum = pgEnum('pig_confidence_grade', mut(CONFIDENCE_GRADES));
export const affiliationKindEnum = pgEnum('pig_affiliation_kind', mut(AFFILIATION_KINDS));
+113
View File
@@ -0,0 +1,113 @@
/**
* User-defined fields.
*
* Every CRM grows fields its designers did not anticipate, and a schema that
* refuses them is a schema that gets worked around in a spreadsheet. The
* variant here carries one idea worth borrowing from Comp AI CRM (MIT, see
* NOTICE): `agentBrief` — a prose instruction telling the agent *how* to fill
* this particular field. A custom field is otherwise opaque to an agent, which
* knows the column exists but nothing about what would constitute a good value.
*/
import {
boolean,
index,
integer,
numeric,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from 'drizzle-orm/pg-core';
import { accounts, contacts } from './crm';
import { demandDeals } from './demand';
import { supplyDeals } from './supply';
import { users } from './identity';
export const fieldDefinitions = pgTable(
'field_definitions',
{
id: uuid('id').primaryKey().defaultRandom(),
/** 'account' | 'contact' | 'demand_deal' | 'supply_deal' */
entity: text('entity').notNull(),
key: text('key').notNull(),
label: text('label').notNull(),
/**
* 'text' | 'long_text' | 'number' | 'date' | 'checkbox' | 'select' |
* 'url' | 'email' | 'user'
*/
type: text('type').notNull().default('text'),
/** Whether the agent may populate this field at all. */
agentFilled: boolean('agent_filled').notNull().default(true),
/**
* How to fill it, in prose. e.g. "The provider's InfiniBand
* oversubscription ratio, as 1:1, 2:1 or 4:1. Only record it if stated
* explicitly — never infer it from marketing copy."
*/
agentBrief: text('agent_brief'),
isRequired: boolean('is_required').notNull().default(false),
showOnTable: boolean('show_on_table').notNull().default(false),
position: integer('position').notNull().default(0),
archivedAt: timestamp('archived_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [uniqueIndex('field_definitions_entity_key').on(t.entity, t.key)],
);
export const fieldOptions = pgTable(
'field_options',
{
id: uuid('id').primaryKey().defaultRandom(),
fieldId: uuid('field_id')
.notNull()
.references(() => fieldDefinitions.id, { onDelete: 'cascade' }),
label: text('label').notNull(),
position: integer('position').notNull().default(0),
archivedAt: timestamp('archived_at', { withTimezone: true }),
},
(t) => [index('field_options_field_idx').on(t.fieldId)],
);
/**
* Values, typed by column rather than serialised into JSON, so that Postgres
* can sort and filter them properly. The uniqueness constraints keep one value
* per field per record.
*/
export const fieldValues = pgTable(
'field_values',
{
id: uuid('id').primaryKey().defaultRandom(),
fieldId: uuid('field_id')
.notNull()
.references(() => fieldDefinitions.id, { onDelete: 'cascade' }),
accountId: uuid('account_id').references(() => accounts.id, { onDelete: 'cascade' }),
contactId: uuid('contact_id').references(() => contacts.id, { onDelete: 'cascade' }),
demandDealId: uuid('demand_deal_id').references(() => demandDeals.id, {
onDelete: 'cascade',
}),
supplyDealId: uuid('supply_deal_id').references(() => supplyDeals.id, {
onDelete: 'cascade',
}),
textValue: text('text_value'),
numberValue: numeric('number_value', { precision: 24, scale: 4 }),
dateValue: timestamp('date_value', { withTimezone: true }),
boolValue: boolean('bool_value'),
optionId: uuid('option_id').references(() => fieldOptions.id, { onDelete: 'set null' }),
userValue: uuid('user_value').references(() => users.id, { onDelete: 'set null' }),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('field_values_account_key').on(t.fieldId, t.accountId),
uniqueIndex('field_values_contact_key').on(t.fieldId, t.contactId),
uniqueIndex('field_values_demand_deal_key').on(t.fieldId, t.demandDealId),
uniqueIndex('field_values_supply_deal_key').on(t.fieldId, t.supplyDealId),
],
);
export type FieldDefinition = typeof fieldDefinitions.$inferSelect;
export type FieldValue = typeof fieldValues.$inferSelect;
+157
View File
@@ -0,0 +1,157 @@
/**
* Identity and authorization.
*
* PIG authenticates with an external identity provider but authorizes from this
* table. The distinction is load-bearing: the auth project may be shared with
* other applications, so "has a valid token" must never imply "may use PIG".
* A token gets you as far as `GET /api/me`, which returns 404 until an
* administrator or a valid invite creates the row below.
*/
import {
boolean,
index,
jsonb,
pgTable,
primaryKey,
text,
timestamp,
uniqueIndex,
uuid,
} from 'drizzle-orm/pg-core';
import { teamEnum, teamRoleEnum } from './enums';
export const users = pgTable(
'users',
{
id: uuid('id').primaryKey().defaultRandom(),
/**
* The external identity provider's subject claim. This is the join to
* authentication and the only thing a bearer token proves.
* Nullable so that a person can be invited before they first sign in.
*/
authSubject: uuid('auth_subject'),
email: text('email').notNull(),
name: text('name').notNull(),
/** Short handle used in mentions and CLI output. */
handle: text('handle'),
avatarUrl: text('avatar_url'),
title: text('title'),
timezone: text('timezone'),
/**
* Platform administration is separate from being a member of any team, and
* separate again from being an employee of the organisation PIG tracks.
* Conflating the three is how an operator quietly ends up recorded as staff
* of a company they do not work for.
*/
isPlatformAdmin: boolean('is_platform_admin').notNull().default(false),
/** Set when the person stops using PIG. Rows are retained for audit. */
deactivatedAt: timestamp('deactivated_at', { withTimezone: true }),
lastSeenAt: timestamp('last_seen_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('users_auth_subject_key').on(t.authSubject),
uniqueIndex('users_email_key').on(t.email),
uniqueIndex('users_handle_key').on(t.handle),
],
);
/**
* Team membership. A person may belong to several teams — in a small company
* the same individual routinely sells capacity and sources it — so this is a
* join table rather than a column on `users`.
*/
export const teamMemberships = pgTable(
'team_memberships',
{
userId: uuid('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
team: teamEnum('team').notNull(),
role: teamRoleEnum('role').notNull().default('member'),
/** The team shown on sign-in when someone belongs to more than one. */
isPrimary: boolean('is_primary').notNull().default(false),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
primaryKey({ columns: [t.userId, t.team] }),
index('team_memberships_team_idx').on(t.team),
],
);
/**
* Invitations. Self-serve profile creation is gated on one of these rather than
* on the identity provider's own signup setting, which may be shared with
* unrelated applications and toggled by someone with no knowledge of PIG.
*/
export const invites = pgTable(
'invites',
{
id: uuid('id').primaryKey().defaultRandom(),
/** Hashed, never stored in the clear. */
codeHash: text('code_hash').notNull(),
/** Optional pinning to one address; null allows any recipient. */
email: text('email'),
team: teamEnum('team'),
role: teamRoleEnum('role').notNull().default('member'),
createdByUserId: uuid('created_by_user_id').references(() => users.id, {
onDelete: 'set null',
}),
expiresAt: timestamp('expires_at', { withTimezone: true }),
/** Number of times this invite may still be redeemed. */
usesRemaining: jsonb('uses_remaining').$type<number>().notNull().default(1),
redeemedByUserId: uuid('redeemed_by_user_id').references(() => users.id, {
onDelete: 'set null',
}),
redeemedAt: timestamp('redeemed_at', { withTimezone: true }),
revokedAt: timestamp('revoked_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('invites_code_hash_key').on(t.codeHash),
index('invites_email_idx').on(t.email),
],
);
/**
* API keys, for the CLI and for MCP clients connecting over HTTP.
*
* An agent acting for a person is not the same principal as that person: it
* gets its own key, its own audit trail, and can be revoked without disturbing
* the human's session. Only the hash is stored, so a leaked database does not
* yield working credentials.
*/
export const apiKeys = pgTable(
'api_keys',
{
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
keyHash: text('key_hash').notNull(),
/** Non-secret leading fragment, so a key can be identified in a list. */
keyPrefix: text('key_prefix').notNull(),
/** Coarse scopes. `read` is the default and is enough for most agent use. */
scopes: jsonb('scopes').$type<string[]>().notNull().default(['read']),
lastUsedAt: timestamp('last_used_at', { withTimezone: true }),
expiresAt: timestamp('expires_at', { withTimezone: true }),
revokedAt: timestamp('revoked_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
uniqueIndex('api_keys_key_hash_key').on(t.keyHash),
index('api_keys_user_idx').on(t.userId),
],
);
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type TeamMembership = typeof teamMemberships.$inferSelect;
export type ApiKey = typeof apiKeys.$inferSelect;
+26
View File
@@ -0,0 +1,26 @@
/**
* The PIG schema.
*
* Read in this order to understand the domain:
*
* ontology (in @pig/core) the vocabulary — teams, stages, tiers
* identity who may use PIG, and as what
* crm accounts, contacts, activities — the ordinary part
* supply sites, priced inventory, capacity we have bought
* demand what customers want, and what we have sold
* allocations the join between the two. The reason PIG exists.
* contracts MSA, DPA, SLA, order forms, obligations
* compliance export control as a predicate on the match
* agent the leased task queue and evidence-bearing facts
* fields user-defined fields
*/
export * from './enums';
export * from './identity';
export * from './crm';
export * from './supply';
export * from './demand';
export * from './allocations';
export * from './contracts';
export * from './compliance';
export * from './agent';
export * from './fields';
+386
View File
@@ -0,0 +1,386 @@
/**
* The supply side: where capacity comes from, what it costs, and what we have
* committed to buy.
*
* This is the half of the business no generic CRM models at all. A supplier is
* not merely an account with a different label — it has physical sites, priced
* inventory that changes hourly, and commitments that keep costing money
* whether or not anyone bought the hours.
*/
import {
boolean,
index,
integer,
jsonb,
numeric,
pgTable,
text,
timestamp,
uniqueIndex,
uuid,
} from 'drizzle-orm/pg-core';
import {
gpuSocketEnum,
interconnectTypeEnum,
recordSourceEnum,
securityTierEnum,
stockStatusEnum,
supplyStageEnum,
} from './enums';
import { accounts, contacts } from './crm';
import { users } from './identity';
/**
* A physical facility. Distinct from the account that sells it, because one
* provider operates many sites with materially different characteristics, and
* because jurisdiction attaches to the building rather than the company.
*/
export const sites = pgTable(
'sites',
{
id: uuid('id').primaryKey().defaultRandom(),
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id, { onDelete: 'cascade' }),
name: text('name').notNull(),
/** Provider's own datacenter identifier, for reconciliation. */
externalDataCenterId: text('external_data_center_id'),
country: text('country'),
countryCode: text('country_code'),
region: text('region'),
city: text('city'),
/**
* Export-control and data-residency jurisdiction. Some capacity legally
* cannot serve some customers, which makes this a matching constraint and
* not a note.
*/
jurisdiction: text('jurisdiction'),
/** Contracted power envelope. The real ceiling on how much can ever land here. */
powerMw: numeric('power_mw', { precision: 10, scale: 3 }),
/** Power usage effectiveness — a cost driver the supplier rarely volunteers. */
pue: numeric('pue', { precision: 4, scale: 2 }),
/** Compliance posture. Enterprise buyers gate on these during procurement. */
certifications: jsonb('certifications').$type<string[]>().notNull().default([]),
/** Observed reliability, maintained by hand or by the agent. */
uptimeHistoryPct: numeric('uptime_history_pct', { precision: 6, scale: 3 }),
notes: text('notes'),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('sites_account_idx').on(t.accountId),
index('sites_country_idx').on(t.countryCode),
],
);
/**
* A priced offer of capacity.
*
* The field names and value shapes deliberately mirror the Prime Intellect
* availability API so that synced rows map across without translation, and so
* that a hand-entered listing from a provider who has no API is directly
* comparable with a synced one.
*
* Listings are volatile — prices and stock move hourly. They are a snapshot of
* what is purchasable, not a record of what we own; that is
* `capacityCommitments` below.
*/
export const inventoryListings = pgTable(
'inventory_listings',
{
id: uuid('id').primaryKey().defaultRandom(),
accountId: uuid('account_id').references(() => accounts.id, { onDelete: 'set null' }),
siteId: uuid('site_id').references(() => sites.id, { onDelete: 'set null' }),
/** Upstream identifiers, used to reconcile a sync without duplicating rows. */
externalCloudId: text('external_cloud_id'),
providerSlug: text('provider_slug'),
/** Kept as free text: new accelerators ship faster than enum migrations. */
gpuType: text('gpu_type').notNull(),
socket: gpuSocketEnum('socket'),
gpuCount: integer('gpu_count').notNull(),
gpuMemoryGb: integer('gpu_memory_gb'),
vcpu: integer('vcpu'),
memoryGb: integer('memory_gb'),
diskGb: integer('disk_gb'),
internetMbps: integer('internet_mbps'),
/**
* The field that decides whether this capacity can train or only serve.
* Ethernet-only hardware sold at a training price is the most common way to
* be overcharged in this market, so it is indexed and surfaced everywhere.
*/
interconnectGbps: integer('interconnect_gbps'),
interconnectType: interconnectTypeEnum('interconnect_type').notNull().default('Unknown'),
region: text('region'),
country: text('country'),
securityTier: securityTierEnum('security_tier').notNull().default('secure_cloud'),
stockStatus: stockStatusEnum('stock_status').notNull().default('Available'),
isSpot: boolean('is_spot').notNull().default(false),
/** Minutes from order to usable. Two orders of magnitude across the market. */
provisioningMinutes: integer('provisioning_minutes'),
/** Hours of prepaid time bundled with the listing, where offered. */
prepaidHours: numeric('prepaid_hours', { precision: 12, scale: 2 }),
/** Money in cents. Never floats — this feeds margin reporting. */
onDemandPriceCents: integer('on_demand_price_cents'),
communityPriceCents: integer('community_price_cents'),
priceIsVariable: boolean('price_is_variable').notNull().default(false),
currency: text('currency').notNull().default('USD'),
images: jsonb('images').$type<string[]>().notNull().default([]),
raw: jsonb('raw').$type<Record<string, unknown>>(),
source: recordSourceEnum('source').notNull().default('manual'),
/** When the upstream last confirmed this. Stale listings mislead sellers. */
observedAt: timestamp('observed_at', { withTimezone: true }).notNull().defaultNow(),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
/**
* Natural key for an upstream sync. A provider's inventory is identified by
* the cloud, the SKU, the socket, the count and the tier; upserting on this
* keeps repeated syncs idempotent instead of accumulating near-duplicates.
*/
uniqueIndex('inventory_listings_external_key').on(
t.externalCloudId,
t.gpuType,
t.socket,
t.gpuCount,
t.securityTier,
),
index('inventory_listings_gpu_type_idx').on(t.gpuType),
index('inventory_listings_stock_idx').on(t.stockStatus),
index('inventory_listings_interconnect_idx').on(t.interconnectType),
index('inventory_listings_account_idx').on(t.accountId),
index('inventory_listings_observed_idx').on(t.observedAt),
],
);
/**
* A supply engagement — the pipeline for bringing a provider on.
*
* Qualification is split into technical and financial diligence because
* accepting capacity is a two-key decision: engineering judges whether the
* cluster can actually do the work, finance judges whether the economics clear.
* Recording who accepted what, and why, is the difference between a decision
* and a vibe.
*/
export const supplyDeals = pgTable(
'supply_deals',
{
id: uuid('id').primaryKey().defaultRandom(),
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id, { onDelete: 'cascade' }),
siteId: uuid('site_id').references(() => sites.id, { onDelete: 'set null' }),
name: text('name').notNull(),
stage: supplyStageEnum('stage').notNull().default('sourced'),
stageChangedAt: timestamp('stage_changed_at', { withTimezone: true }).notNull().defaultNow(),
ownerUserId: uuid('owner_user_id').references(() => users.id, { onDelete: 'set null' }),
primaryContactId: uuid('primary_contact_id').references(() => contacts.id, {
onDelete: 'set null',
}),
/** What is on offer. */
gpuType: text('gpu_type'),
gpuCount: integer('gpu_count'),
interconnectType: interconnectTypeEnum('interconnect_type'),
targetCostPerGpuHourCents: integer('target_cost_per_gpu_hour_cents'),
termMonths: integer('term_months'),
availableFrom: timestamp('available_from', { withTimezone: true }),
/**
* The two diligence gates, recorded separately and attributably.
* A null verdict means the gate has not been reached, which is different
* from having been considered and passed.
*/
technicalVerdict: text('technical_verdict'),
technicalVerdictBy: uuid('technical_verdict_by').references(() => users.id, {
onDelete: 'set null',
}),
technicalVerdictAt: timestamp('technical_verdict_at', { withTimezone: true }),
technicalNotes: text('technical_notes'),
financialVerdict: text('financial_verdict'),
financialVerdictBy: uuid('financial_verdict_by').references(() => users.id, {
onDelete: 'set null',
}),
financialVerdictAt: timestamp('financial_verdict_at', { withTimezone: true }),
financialNotes: text('financial_notes'),
/** Recorded when the stage becomes `rejected`. Rejections teach. */
rejectionReason: text('rejection_reason'),
closedAt: timestamp('closed_at', { withTimezone: true }),
lastActivityAt: timestamp('last_activity_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('supply_deals_account_idx').on(t.accountId),
index('supply_deals_stage_idx').on(t.stage),
index('supply_deals_owner_idx').on(t.ownerUserId),
],
);
/**
* Capacity we have actually committed to buy.
*
* This is the cost side of the margin ledger. Unlike a listing, a commitment
* is a liability: the hours are paid for whether or not anyone uses them. That
* is precisely why idle capacity is worth alerting on, and why margin is
* computed against the full commitment rather than only the hours that sold.
*/
export const capacityCommitments = pgTable(
'capacity_commitments',
{
id: uuid('id').primaryKey().defaultRandom(),
accountId: uuid('account_id')
.notNull()
.references(() => accounts.id, { onDelete: 'restrict' }),
siteId: uuid('site_id').references(() => sites.id, { onDelete: 'set null' }),
supplyDealId: uuid('supply_deal_id').references(() => supplyDeals.id, {
onDelete: 'set null',
}),
name: text('name').notNull(),
gpuType: text('gpu_type').notNull(),
socket: gpuSocketEnum('socket'),
gpuCount: integer('gpu_count').notNull(),
interconnectType: interconnectTypeEnum('interconnect_type').notNull().default('Unknown'),
securityTier: securityTierEnum('security_tier').notNull().default('secure_cloud'),
startsAt: timestamp('starts_at', { withTimezone: true }).notNull(),
endsAt: timestamp('ends_at', { withTimezone: true }).notNull(),
/**
* Total contracted GPU-hours. Stored explicitly rather than derived from
* count × duration, because real contracts include ramp periods,
* maintenance windows and holdbacks that no formula predicts.
*/
totalGpuHours: numeric('total_gpu_hours', { precision: 16, scale: 2 }).notNull(),
costPerGpuHourCents: integer('cost_per_gpu_hour_cents').notNull(),
currency: text('currency').notNull().default('USD'),
/**
* The capacity SHAPE — how much is actually held at each point in time.
*
* A commitment is not a rectangle. Real contracts ramp over tranches, step
* down at renegotiation checkpoints, and hold different node counts in
* different months. A single start/end/total flattens all of that and then
* reports availability that does not exist in the month someone wants it.
*
* Modelled as parallel arrays of interval boundaries and the quantity held
* during each interval — the primitive used by compute exchanges that
* actually trade this. Availability at any instant is then simply:
*
* available(t) = shapeQuantityAt(t) Σ overlapping allocations(t)
*
* `startsAt`, `endsAt` and `gpuCount` above remain as the coarse envelope,
* for indexing and for the common flat case. Where `shape` is present it
* is authoritative.
*/
shape: jsonb('shape').$type<{
/** ISO-8601 boundaries, ascending. n+1 entries for n intervals. */
intervals: string[];
/** GPUs held during each interval. Length = intervals.length 1. */
quantities: number[];
}>(),
/**
* Placement constraints. Contiguity and adjacency are commercial terms
* here, not deployment details: a customer needing one contiguous 512-GPU
* block cannot use two 256-GPU blocks in different halls, and a deal dies
* on exactly that distinction.
*/
colocateWith: jsonb('colocate_with').$type<string[]>().notNull().default([]),
isContiguous: boolean('is_contiguous').notNull().default(true),
/** Contractual floor: the minimum we owe regardless of usage. */
minimumSpendCents: integer('minimum_spend_cents'),
isAutoRenew: boolean('is_auto_renew').notNull().default(false),
/** Days of notice required to exit. Drives renewal alerting. */
noticeDays: integer('notice_days'),
/**
* Take-or-pay floor as a percentage of contracted volume — the share we
* owe whether or not we draw it. This is the field that turns a
* commitment from an option into a liability, and it is why idle capacity
* is worth alerting on rather than merely noting.
*/
takeOrPayFloorPct: numeric('take_or_pay_floor_pct', { precision: 6, scale: 2 }),
/** Share of total value paid upfront. Prepayment is common and material. */
prepaidPct: numeric('prepaid_pct', { precision: 6, scale: 2 }),
prepaidAmountCents: integer('prepaid_amount_cents'),
/**
* Depreciation assumptions for any hardware we own against this block.
*
* Useful life is the single largest swing variable in compute unit
* economics — operators in this market publish anything from four to six
* years for identical hardware, and the same contract can show roughly
* 10% or 25% contribution margin depending purely on that choice. Storing
* the assumption per block, rather than as a company-wide constant, is
* what makes margin recomputable under different scenarios instead of
* being an unexaminable number.
*/
usefulLifeYears: numeric('useful_life_years', { precision: 4, scale: 2 }),
salvageValuePct: numeric('salvage_value_pct', { precision: 6, scale: 2 }),
depreciationStartAt: timestamp('depreciation_start_at', { withTimezone: true }),
/**
* Cost of capital for THIS block, in basis points.
*
* An attribute of the block rather than of the company, because blocks are
* funded by different instruments at very different rates — blended debt
* costs across operators in this market span roughly fourfold. Applying a
* single corporate rate makes per-deal margin wrong in both directions.
*/
costOfCapitalBps: integer('cost_of_capital_bps'),
financingInstrument: text('financing_instrument'),
/**
* Deliberate oversubscription. Some capacity is sold beyond 100% on the
* assumption not every buyer draws their full reservation. Recording the
* intent stops the margin view from flagging a policy as a bug.
*/
oversubscriptionPct: numeric('oversubscription_pct', { precision: 6, scale: 2 })
.notNull()
.default('0'),
notes: text('notes'),
terminatedAt: timestamp('terminated_at', { withTimezone: true }),
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
},
(t) => [
index('capacity_commitments_account_idx').on(t.accountId),
index('capacity_commitments_gpu_type_idx').on(t.gpuType),
index('capacity_commitments_window_idx').on(t.startsAt, t.endsAt),
],
);
export type Site = typeof sites.$inferSelect;
export type InventoryListing = typeof inventoryListings.$inferSelect;
export type NewInventoryListing = typeof inventoryListings.$inferInsert;
export type SupplyDeal = typeof supplyDeals.$inferSelect;
export type CapacityCommitment = typeof capacityCommitments.$inferSelect;
export type NewCapacityCommitment = typeof capacityCommitments.$inferInsert;
+5
View File
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": { "noEmit": true },
"include": ["src/**/*.ts", "drizzle.config.ts"]
}