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:
@@ -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;
|
||||
Reference in New Issue
Block a user