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,66 @@
|
||||
# ---------------------------------------------------------------------------
|
||||
# PIG — environment
|
||||
# Copy to .env and fill in. Never commit .env.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# --- Database ---------------------------------------------------------------
|
||||
# PIG owns this database exclusively. Do not point it at a database shared with
|
||||
# another application.
|
||||
DATABASE_URL=postgres://pig:CHANGEME@localhost:5432/pig
|
||||
|
||||
# --- Auth (Supabase) --------------------------------------------------------
|
||||
# PIG uses Supabase for authentication ONLY. It stores no passwords and issues
|
||||
# no sessions of its own; it verifies incoming JWTs against the project JWKS.
|
||||
#
|
||||
# IMPORTANT: authorization does NOT follow from having a Supabase account.
|
||||
# A user must also have a row in PIG's `users` table. If this Supabase project
|
||||
# is shared with another application, that application's users get nothing here
|
||||
# until they are explicitly invited.
|
||||
SUPABASE_URL=https://YOUR_PROJECT_REF.supabase.co
|
||||
SUPABASE_ANON_KEY=
|
||||
# Service key is only needed for administrative user provisioning. Omit it and
|
||||
# PIG runs fine in invite-only mode. Treat it as the most powerful secret here.
|
||||
SUPABASE_SERVICE_KEY=
|
||||
|
||||
# --- Application ------------------------------------------------------------
|
||||
PIG_PORT=8920
|
||||
PIG_PUBLIC_URL=http://localhost:8920
|
||||
NODE_ENV=development
|
||||
|
||||
# Comma-separated emails granted platform-admin rights.
|
||||
# Every address listed here MUST already have an account. An address listed but
|
||||
# unregistered is a standing offer of admin to whoever claims it first.
|
||||
PIG_ADMIN_EMAILS=
|
||||
|
||||
# Invite code gating self-serve profile creation. Rotate freely.
|
||||
PIG_INVITE_CODE=
|
||||
|
||||
# --- Prime Intellect compute API -------------------------------------------
|
||||
# Used to sync GPU availability into `inventory_listings`.
|
||||
# Mint a key at https://app.primeintellect.ai/dashboard/tokens with the
|
||||
# NARROWEST scope that works: `Availability -> Read`. PIG never provisions
|
||||
# infrastructure and must not hold a key that could. Set an expiry.
|
||||
PRIME_API_KEY=
|
||||
PRIME_API_BASE=https://api.primeintellect.ai
|
||||
# Rate limits are undocumented upstream; the sync backs off empirically.
|
||||
PRIME_SYNC_ENABLED=false
|
||||
PRIME_SYNC_INTERVAL_MINUTES=30
|
||||
|
||||
# --- Piggy (the in-app agent) ----------------------------------------------
|
||||
# Piggy drains a leased queue in Postgres. Leave the key unset to run PIG with
|
||||
# no agent at all — every human-facing feature works without it.
|
||||
ANTHROPIC_API_KEY=
|
||||
PIGGY_ENABLED=false
|
||||
PIGGY_MODEL=claude-sonnet-5
|
||||
PIGGY_LEASE_SECONDS=300
|
||||
|
||||
# --- Slack ------------------------------------------------------------------
|
||||
SLACK_BOT_TOKEN=
|
||||
SLACK_SIGNING_SECRET=
|
||||
SLACK_APP_TOKEN=
|
||||
|
||||
# --- Buzz (https://github.com/block/buzz) -----------------------------------
|
||||
# Buzz agents reach PIG through the MCP server, so no PIG-specific credential is
|
||||
# required. These are only for PIG pushing notifications into a Buzz relay.
|
||||
BUZZ_RELAY_URL=
|
||||
BUZZ_SECRET_KEY=
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
node_modules/
|
||||
dist/
|
||||
build/
|
||||
.turbo/
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment — never commit secrets
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Local data
|
||||
*.log
|
||||
.DS_Store
|
||||
coverage/
|
||||
.vite/
|
||||
.cache/
|
||||
|
||||
# Postgres volume mounts used by local compose
|
||||
deploy/pgdata/
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -0,0 +1,47 @@
|
||||
PIG — Prime Intellect Growth
|
||||
Copyright 2026 The PIG Authors
|
||||
|
||||
This product includes software developed by The PIG Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
---
|
||||
|
||||
ACKNOWLEDGEMENTS
|
||||
|
||||
Several architectural ideas in this project were studied from, and are
|
||||
gratefully credited to, the following open-source projects. No source code
|
||||
was copied from them; the debt is one of design.
|
||||
|
||||
Comp AI CRM (https://github.com/trycompai/crm) — MIT License.
|
||||
The evidence-banded fact model, the leased database-backed agent task
|
||||
queue, the agent-brief pattern on user-defined fields, and the
|
||||
"intelligence never lives in the API" separation.
|
||||
|
||||
Buzz (https://github.com/block/buzz) — Apache License 2.0.
|
||||
The agent-as-workspace-member model that informed PIG's treatment of
|
||||
agents as first-class principals with their own identity and audit trail.
|
||||
|
||||
---
|
||||
|
||||
DATA PROVENANCE
|
||||
|
||||
PIG ships with seed data describing publicly documented people and companies.
|
||||
Every seeded person record carries a `confidence` grade and a `source_url`
|
||||
citing where the claim came from. No email addresses are included, inferred,
|
||||
or generated: none were published by the subjects, and guessing them would be
|
||||
both unreliable and inappropriate.
|
||||
|
||||
Seed data reflects public sources as of August 2026 and will go stale. It is
|
||||
provided to make the application legible on first run, not as an assertion of
|
||||
fact about any individual. See docs/seed-data.md.
|
||||
@@ -0,0 +1,179 @@
|
||||
<div align="center">
|
||||
|
||||
# 🐷 PIG — Prime Intellect Growth
|
||||
|
||||
**An open-source, agent-native CRM for two-sided AI-compute companies.**
|
||||
|
||||
[](./LICENSE)
|
||||
|
||||
*Self-hostable. Auditable. Built for teams that buy compute on one side and sell it on the other.*
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
## Why this exists
|
||||
|
||||
A company that aggregates GPU capacity and resells it does not run one pipeline.
|
||||
It runs two, and its business is the spread between them.
|
||||
|
||||
Generic CRMs — Salesforce, HubSpot, Attio — model a single pipeline of deals
|
||||
against companies. They have no concept of **inventory**, no concept of a
|
||||
**commitment you already bought and are paying for**, and therefore no way to
|
||||
answer the question the business actually turns on:
|
||||
|
||||
> Which contracted capacity is sold, to whom, at what margin — and what is idle
|
||||
> right now?
|
||||
|
||||
PIG is built around that question. One table, [`allocations`](./packages/db/src/schema/allocations.ts),
|
||||
joins a `capacity_commitment` (what you bought from a provider) to a
|
||||
`demand_deal` (what you sold to a customer). Revenue minus cost is margin per
|
||||
GPU-hour. Committed capacity with no allocation is money burning. Everything
|
||||
else in PIG is ordinary CRM plumbing that exists to keep that ledger honest.
|
||||
|
||||
## Who it's for
|
||||
|
||||
PIG models three teams, because two-sided compute companies have three
|
||||
constituencies competing for the same scarce capacity:
|
||||
|
||||
| Team | Job to be done |
|
||||
|---|---|
|
||||
| **Supply** | Source, qualify, price, and contract GPU capacity from providers |
|
||||
| **Demand** | Sell compute and post-training; renew and expand accounts |
|
||||
| **Research** | Consume capacity internally — real burn, no revenue |
|
||||
|
||||
Research is a first-class tenant rather than an afterthought. Internal research
|
||||
burn competes with revenue for the same GPUs, and margin math that cannot see it
|
||||
is wrong.
|
||||
|
||||
The team set is configurable. PIG ships with these three because they match the
|
||||
structure of the company it was designed for, not because they are universal.
|
||||
|
||||
## Agent-native, not agent-decorated
|
||||
|
||||
PIG is a first-class application for agents *and* for humans, and neither is a
|
||||
degraded view of the other.
|
||||
|
||||
- **An MCP server** ([`apps/mcp`](./apps/mcp)) exposes the CRM over both stdio
|
||||
and Streamable HTTP. Any MCP client connects: **Claude Code**, **Codex**,
|
||||
**[prime-agent](https://github.com/PrimeIntellect-ai/prime-agent)**, or a
|
||||
**[Buzz](https://github.com/block/buzz)** workspace agent via its ACP bridge.
|
||||
Each team member points their own agent at PIG and works from the terminal.
|
||||
- **Piggy**, the in-app agent, drains a leased database queue rather than being
|
||||
called over HTTP — so work survives the agent being down, and every action it
|
||||
takes is recorded with an idempotency key.
|
||||
- **Every agent-derived fact carries evidence.** Enrichment writes to a `facts`
|
||||
table with a confidence score, a band (verified / probable / possible), a
|
||||
source URL, and a status. Strong signals apply automatically; weak ones become
|
||||
proposals a human approves. A CRM that lets an agent write unattributed claims
|
||||
into the record is a hallucination store, not a database.
|
||||
|
||||
### The architectural rule
|
||||
|
||||
> **Intelligence never lives in the API.**
|
||||
|
||||
The API does HTTP, auth, validation, and sync. All research, enrichment,
|
||||
scoring, and identity matching lives in the agent. They communicate through a
|
||||
table, never a direct call. This separation is borrowed from
|
||||
[Comp AI CRM](https://github.com/trycompai/crm) and it is the single most
|
||||
load-bearing decision in the codebase.
|
||||
|
||||
## What makes it compute-native
|
||||
|
||||
- **`inventory_listings`** mirrors the Prime Intellect availability API
|
||||
field-for-field — `gpuType`, `socket`, `interconnectType`, `stockStatus`,
|
||||
`security` (secure vs community cloud), `prices.onDemand`, `provisioningTime`.
|
||||
Sync is a straight mapping, not an ETL project.
|
||||
- **`capacity_commitments`** records what you bought: term, GPU-hours,
|
||||
cost per GPU-hour, floor and ceiling.
|
||||
- **`contracts`** is polymorphic over party and type — MSA, DPA, SLA, order
|
||||
form, capacity commitment — because the supply side negotiates heavyweight
|
||||
paper while the self-serve demand side runs on a reliability tier and a
|
||||
credits policy instead of a signed uptime guarantee.
|
||||
- **Two real pipelines**, with stages taken from how this market actually
|
||||
operates rather than invented:
|
||||
|
||||
```
|
||||
Demand: qualification → legal → scoping → proposal → procurement
|
||||
→ POC → deployment → expansion
|
||||
|
||||
Supply: sourced → qualifying → technical diligence → financial diligence
|
||||
→ pricing → contracting → onboarding → live → renewal
|
||||
```
|
||||
|
||||
Note that **legal sits second** in the demand pipeline. MSA and DPA execution
|
||||
gates the deal rather than closing it. Most CRMs put contracts at the end and
|
||||
are wrong about it for this market.
|
||||
|
||||
## Stack
|
||||
|
||||
| Layer | Choice |
|
||||
|---|---|
|
||||
| Web | React + Vite + TypeScript, Tailwind, shadcn/ui, light + dark |
|
||||
| API | Hono + tRPC on Node 22+ |
|
||||
| Database | PostgreSQL 16, Drizzle ORM |
|
||||
| Auth | Supabase (JWT verification only — PIG stores no passwords) |
|
||||
| Agent | Piggy — a worker draining a leased task queue |
|
||||
| MCP | `@modelcontextprotocol/sdk` — stdio + Streamable HTTP |
|
||||
| Deploy | Docker Compose behind any reverse proxy |
|
||||
|
||||
Authorization comes from PIG's own `users` table, never from the mere existence
|
||||
of an auth account. An identity provider that PIG shares with another
|
||||
application must not grant access here.
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
git clone <this-repo> pig && cd pig
|
||||
npm install
|
||||
cp .env.example .env # then edit it
|
||||
npm run db:migrate
|
||||
npm run db:seed # optional — public, sourced, confidence-graded
|
||||
npm run dev:api # :8920
|
||||
npm run dev:web # :5173
|
||||
```
|
||||
|
||||
Connect an agent:
|
||||
|
||||
```bash
|
||||
claude mcp add pig -- npx -y @pig/mcp # stdio
|
||||
# or point any MCP client at https://<your-host>/mcp
|
||||
```
|
||||
|
||||
## Repository layout
|
||||
|
||||
```
|
||||
apps/
|
||||
web/ React + Vite front end
|
||||
api/ Hono + tRPC API, Supabase JWT verification
|
||||
mcp/ MCP server — stdio and Streamable HTTP
|
||||
packages/
|
||||
db/ Drizzle schema, migrations, seed
|
||||
core/ Shared domain types and the ontology
|
||||
prime/ Typed client for the Prime Intellect compute API
|
||||
docs/ Ontology, deployment, seed-data provenance
|
||||
deploy/ Compose files and reverse-proxy snippets
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Ontology](./docs/ontology.md) — the domain model, and why it is shaped this way
|
||||
- [Seed data provenance](./docs/seed-data.md) — every claim, graded and cited
|
||||
- [Agent integration](./docs/agents.md) — Claude Code, Codex, prime-agent, Buzz
|
||||
- [Deployment](./docs/deploy.md) — self-hosting
|
||||
|
||||
## A note on seed data
|
||||
|
||||
PIG ships with a roster of publicly documented people so the application is
|
||||
legible on first run. Every record carries a confidence grade and a source URL.
|
||||
**No email addresses are included or inferred.** Records that could not be
|
||||
independently sourced are marked as such rather than quietly presented as fact,
|
||||
and people who are demonstrably *not* staff — alumni, residency participants —
|
||||
are labelled accordingly. See [docs/seed-data.md](./docs/seed-data.md).
|
||||
|
||||
If you are seeded here and would rather not be, open an issue and it will be
|
||||
removed.
|
||||
|
||||
## Licence
|
||||
|
||||
Apache License 2.0 — see [LICENSE](./LICENSE) and [NOTICE](./NOTICE).
|
||||
Generated
+1822
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "pig",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "PIG — Prime Intellect Growth. An open-source, agent-native CRM for two-sided AI-compute companies.",
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"apps/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npm run build --workspaces --if-present",
|
||||
"typecheck": "npm run typecheck --workspaces --if-present",
|
||||
"test": "npm run test --workspaces --if-present",
|
||||
"lint": "npm run lint --workspaces --if-present",
|
||||
"dev:api": "npm run dev -w @pig/api",
|
||||
"dev:web": "npm run dev -w @pig/web",
|
||||
"dev:mcp": "npm run dev -w @pig/mcp",
|
||||
"db:generate": "npm run generate -w @pig/db",
|
||||
"db:migrate": "npm run migrate -w @pig/db",
|
||||
"db:seed": "npm run seed -w @pig/db"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.7.2",
|
||||
"@types/node": "^22.10.2",
|
||||
"tsx": "^4.19.2"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './ontology';
|
||||
export * from './margin';
|
||||
@@ -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, 0–1. */
|
||||
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, 0–1. 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);
|
||||
}
|
||||
@@ -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];
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist",
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1786585246686,
|
||||
"tag": "0000_initial",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -0,0 +1 @@
|
||||
export * from './client';
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
/** 0–1. 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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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, 0–1. 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;
|
||||
@@ -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));
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": { "noEmit": true },
|
||||
"include": ["src/**/*.ts", "drizzle.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"isolatedModules": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user