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
+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];