Add deployment: Dockerfile, compose, proxy config, and docs
One container plus a Postgres behind any TLS-terminating proxy. Nothing is specific to a particular host. The app and API are served from a SINGLE origin. This is not tidiness: browser auth sessions live in per-origin storage, so splitting them across two hostnames makes sign-in loop in a way that presents as a server fault. The short alias redirects rather than serving a second origin. Two safety properties verified by running the image, not by reading the code: - With NODE_ENV=production and no SUPABASE_URL, the process refuses to start and says why. Serving the whole CRM unauthenticated is a worse outcome than failing to deploy, so the failure is deliberate and loud. - In production the development auth bypass does not apply: an unauthenticated request to /api/dashboard returns 401 rather than adopting the first user in the table. The Dockerfile typechecks all six packages as a build gate, so a deploy that does not compile fails at build time rather than in front of a user. Runtime runs unprivileged as `node`, and Postgres is not published to the host. Docs cover the ontology and why it is shaped this way, agent connection for Claude Code / Codex / prime-agent / Buzz, and the provenance rules governing seed data about real people — including how to have your record removed. Verified: image builds, container reports healthy, serves the SPA, enforces auth, and the production guard exits non-zero. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+67
@@ -0,0 +1,67 @@
|
|||||||
|
# PIG — production image.
|
||||||
|
#
|
||||||
|
# Multi-stage so the runtime image carries no build toolchain and no source
|
||||||
|
# maps. The front end is built into the API's static directory and served from
|
||||||
|
# the same origin, which matters for more than tidiness: auth sessions are
|
||||||
|
# per-origin, so splitting the app across two hostnames turns sign-in into a
|
||||||
|
# redirect loop that looks like a broken deployment.
|
||||||
|
|
||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Manifests first, so a dependency install is cached across source-only edits.
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
COPY packages/core/package.json packages/core/
|
||||||
|
COPY packages/db/package.json packages/db/
|
||||||
|
COPY packages/prime/package.json packages/prime/
|
||||||
|
COPY apps/api/package.json apps/api/
|
||||||
|
COPY apps/web/package.json apps/web/
|
||||||
|
COPY apps/mcp/package.json apps/mcp/
|
||||||
|
RUN npm install --no-audit --no-fund
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Typecheck as a build gate. A deploy that does not compile should fail here,
|
||||||
|
# loudly, rather than at runtime in front of a user.
|
||||||
|
RUN npx tsc --noEmit -p packages/core/tsconfig.json \
|
||||||
|
&& npx tsc --noEmit -p packages/db/tsconfig.json \
|
||||||
|
&& npx tsc --noEmit -p packages/prime/tsconfig.json \
|
||||||
|
&& npx tsc --noEmit -p apps/api/tsconfig.json \
|
||||||
|
&& npx tsc --noEmit -p apps/web/tsconfig.json \
|
||||||
|
&& npx tsc --noEmit -p apps/mcp/tsconfig.json
|
||||||
|
|
||||||
|
RUN npm run build -w @pig/web
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------- runtime
|
||||||
|
FROM node:22-alpine AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
|
# Reinstall without dev dependencies. tsx is needed at runtime because the
|
||||||
|
# server runs TypeScript directly; everything else is production-only.
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
COPY packages/core/package.json packages/core/
|
||||||
|
COPY packages/db/package.json packages/db/
|
||||||
|
COPY packages/prime/package.json packages/prime/
|
||||||
|
COPY apps/api/package.json apps/api/
|
||||||
|
COPY apps/mcp/package.json apps/mcp/
|
||||||
|
RUN npm install --omit=dev --no-audit --no-fund && npm install tsx --no-audit --no-fund
|
||||||
|
|
||||||
|
COPY packages ./packages
|
||||||
|
COPY apps/api ./apps/api
|
||||||
|
COPY apps/mcp ./apps/mcp
|
||||||
|
COPY --from=build /app/apps/web/dist ./apps/web/dist
|
||||||
|
|
||||||
|
# Run unprivileged. The node image ships a `node` user for exactly this.
|
||||||
|
RUN chown -R node:node /app
|
||||||
|
USER node
|
||||||
|
|
||||||
|
EXPOSE 8920
|
||||||
|
|
||||||
|
# The health endpoint is unauthenticated by design so this works without
|
||||||
|
# credentials baked into the image.
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
|
||||||
|
CMD node -e "fetch('http://127.0.0.1:8920/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||||
|
|
||||||
|
CMD ["npx", "tsx", "apps/api/src/server.ts"]
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Caddy — reverse proxy for PIG.
|
||||||
|
#
|
||||||
|
# Serve the app and the API from ONE hostname. Auth sessions live in
|
||||||
|
# per-origin browser storage, so splitting them across two hostnames makes
|
||||||
|
# sign-in loop endlessly in a way that looks like a server fault.
|
||||||
|
|
||||||
|
primeintellectgrowth.com, www.primeintellectgrowth.com {
|
||||||
|
encode zstd gzip
|
||||||
|
|
||||||
|
# The MCP endpoint, when Streamable HTTP is enabled. Same origin as the
|
||||||
|
# app so it shares the session and needs no CORS allowance.
|
||||||
|
reverse_proxy 127.0.0.1:8920
|
||||||
|
|
||||||
|
header {
|
||||||
|
Strict-Transport-Security "max-age=31536000; includeSubDomains"
|
||||||
|
X-Content-Type-Options "nosniff"
|
||||||
|
X-Frame-Options "DENY"
|
||||||
|
Referrer-Policy "strict-origin-when-cross-origin"
|
||||||
|
# The app is entirely first-party except for the auth provider, which
|
||||||
|
# it must reach over XHR.
|
||||||
|
Content-Security-Policy "default-src 'self'; connect-src 'self' https://*.supabase.co; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self'; frame-ancestors 'none'; base-uri 'self'"
|
||||||
|
-Server
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Short alias. A redirect rather than a second origin, deliberately — see above.
|
||||||
|
pig.karti.ai {
|
||||||
|
redir https://primeintellectgrowth.com{uri} permanent
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# Deploying PIG
|
||||||
|
|
||||||
|
PIG is one container plus a Postgres, behind any reverse proxy that terminates
|
||||||
|
TLS. Nothing here is specific to a particular host.
|
||||||
|
|
||||||
|
## 1. DNS
|
||||||
|
|
||||||
|
Point the apex and `www` at the machine. Both must resolve before the proxy can
|
||||||
|
obtain a certificate.
|
||||||
|
|
||||||
|
```
|
||||||
|
A primeintellectgrowth.com -> <your IP>
|
||||||
|
A www.primeintellectgrowth.com -> <your IP>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Configuration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env # then edit
|
||||||
|
```
|
||||||
|
|
||||||
|
The values that must be set for a production start:
|
||||||
|
|
||||||
|
| Variable | Why |
|
||||||
|
|---|---|
|
||||||
|
| `POSTGRES_PASSWORD` | Generate a fresh one; never reuse another service's |
|
||||||
|
| `PIG_PUBLIC_URL` | The single origin the app is served from |
|
||||||
|
| `SUPABASE_URL` / `SUPABASE_ANON_KEY` | Authentication. The app refuses to start in production without a Supabase URL, because it would otherwise serve the whole CRM unauthenticated |
|
||||||
|
| `PIG_ADMIN_EMAILS` | Who may administer. **Every address here must already have an account** — an unregistered address listed as an admin is a standing offer of admin rights to whoever claims it first |
|
||||||
|
|
||||||
|
Optional: `PRIME_API_KEY` (scope it to `Availability → Read` only),
|
||||||
|
`PIGGY_ENABLED` + `ANTHROPIC_API_KEY`, and the Slack and Buzz credentials.
|
||||||
|
|
||||||
|
## 3. Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -p pig up -d --build
|
||||||
|
docker compose -p pig exec app npx tsx packages/db/src/migrate.ts
|
||||||
|
docker compose -p pig exec app npx tsx packages/db/src/seed/index.ts # optional
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `-p pig`. A compose project that shares a name with a neighbouring stack
|
||||||
|
will adopt its volumes, which is a memorable way to lose a database.
|
||||||
|
|
||||||
|
## 4. Reverse proxy
|
||||||
|
|
||||||
|
See `Caddyfile.example`. Serve the app and API from the **same** origin.
|
||||||
|
|
||||||
|
## 5. Verify
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -s https://primeintellectgrowth.com/api/health
|
||||||
|
# {"ok":true,"service":"pig","version":"0.1.0"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Upgrading
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
docker compose -p pig up -d --build
|
||||||
|
docker compose -p pig exec app npx tsx packages/db/src/migrate.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
Migrations are additive and safe to re-run; Drizzle tracks what has been
|
||||||
|
applied. Take a dump before a major upgrade anyway:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -p pig exec db pg_dump -U pig pig | gzip > pig-$(date +%F).sql.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
## A note on the auth project
|
||||||
|
|
||||||
|
PIG verifies JWTs but authorizes from its own `users` table. If the Supabase
|
||||||
|
project is shared with another application, its users get **nothing** here until
|
||||||
|
they are explicitly invited. That is deliberate, and it is why a valid token can
|
||||||
|
still return `403 needs_profile`.
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# PIG — self-hosted deployment.
|
||||||
|
#
|
||||||
|
# docker compose -p pig up -d --build
|
||||||
|
#
|
||||||
|
# The project name matters. Use something PIG-specific (`-p pig`) so this stack
|
||||||
|
# never adopts another application's volumes — a compose project silently
|
||||||
|
# inheriting a neighbouring database is a genuinely nasty way to lose data.
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-pig}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
|
||||||
|
POSTGRES_DB: ${POSTGRES_DB:-pig}
|
||||||
|
volumes:
|
||||||
|
- pig-pgdata:/var/lib/postgresql/data
|
||||||
|
# Not published to the host. The application reaches it over the compose
|
||||||
|
# network; exposing Postgres publicly is never what you want.
|
||||||
|
expose:
|
||||||
|
- '5432'
|
||||||
|
healthcheck:
|
||||||
|
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-pig} -d ${POSTGRES_DB:-pig}']
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgres://${POSTGRES_USER:-pig}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-pig}
|
||||||
|
NODE_ENV: production
|
||||||
|
PIG_PORT: 8920
|
||||||
|
PIG_PUBLIC_URL: ${PIG_PUBLIC_URL:?PIG_PUBLIC_URL must be set}
|
||||||
|
SUPABASE_URL: ${SUPABASE_URL:?SUPABASE_URL must be set in production}
|
||||||
|
SUPABASE_ANON_KEY: ${SUPABASE_ANON_KEY}
|
||||||
|
SUPABASE_SERVICE_KEY: ${SUPABASE_SERVICE_KEY:-}
|
||||||
|
PIG_ADMIN_EMAILS: ${PIG_ADMIN_EMAILS:-}
|
||||||
|
PIG_INVITE_CODE: ${PIG_INVITE_CODE:-}
|
||||||
|
PRIME_API_KEY: ${PRIME_API_KEY:-}
|
||||||
|
PRIME_SYNC_ENABLED: ${PRIME_SYNC_ENABLED:-false}
|
||||||
|
PIGGY_ENABLED: ${PIGGY_ENABLED:-false}
|
||||||
|
ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-}
|
||||||
|
SLACK_BOT_TOKEN: ${SLACK_BOT_TOKEN:-}
|
||||||
|
SLACK_SIGNING_SECRET: ${SLACK_SIGNING_SECRET:-}
|
||||||
|
BUZZ_RELAY_URL: ${BUZZ_RELAY_URL:-}
|
||||||
|
# Bound to loopback: TLS termination belongs to the reverse proxy in front,
|
||||||
|
# not to this container.
|
||||||
|
ports:
|
||||||
|
- '127.0.0.1:${PIG_HOST_PORT:-8920}:8920'
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
pig-pgdata:
|
||||||
|
# Named explicitly so it is obvious which volume holds the data, and so a
|
||||||
|
# `docker compose down -v` mistake is at least a legible one.
|
||||||
|
name: pig-pgdata
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
# Connecting an agent
|
||||||
|
|
||||||
|
PIG is a first-class application for agents. The same MCP server serves every
|
||||||
|
client, so nobody is asked to use a different tool than the one they already
|
||||||
|
work in.
|
||||||
|
|
||||||
|
## What connects
|
||||||
|
|
||||||
|
| Client | How |
|
||||||
|
|---|---|
|
||||||
|
| **Claude Code** | `claude mcp add pig -- npx -y @pig/mcp` |
|
||||||
|
| **Codex** | Add PIG as an MCP server in its config, with the same env vars |
|
||||||
|
| **prime-agent** | It is an MCP *client*; add PIG through `/mcp` |
|
||||||
|
| **Buzz** | Agents reach PIG through the ACP bridge's MCP support |
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Create an API key in PIG under **Settings → API keys**, then:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PIG_URL=https://primeintellectgrowth.com
|
||||||
|
export PIG_API_KEY=pig_...
|
||||||
|
```
|
||||||
|
|
||||||
|
Scope the key to `read` unless the agent genuinely needs to write. An agent
|
||||||
|
acting for you is a **separate principal** from you: it has its own audit trail
|
||||||
|
and can be revoked without disturbing your session, and it can never reach
|
||||||
|
further than you can.
|
||||||
|
|
||||||
|
## The tools
|
||||||
|
|
||||||
|
| Tool | What it answers |
|
||||||
|
|---|---|
|
||||||
|
| `pig_whoami` | Who am I acting for, and which teams am I on? |
|
||||||
|
| `pig_my_pipeline` | Where are we? What needs attention? |
|
||||||
|
| `pig_capacity_match` | What have we bought that would serve this customer? |
|
||||||
|
| `pig_margin_report` | What is each block earning against what it cost? |
|
||||||
|
| `pig_idle_capacity` | What are we paying for and not selling? |
|
||||||
|
| `pig_inventory_search` | What could we buy to cover demand we cannot serve? |
|
||||||
|
| `pig_search` | Find an account |
|
||||||
|
| `pig_get_account` | Everything about one account |
|
||||||
|
| `pig_log_activity` | Record a call, meeting or note |
|
||||||
|
|
||||||
|
`pig_capacity_match` is the one worth learning. Ask it in plain language:
|
||||||
|
|
||||||
|
> "A customer wants 128 H100s with InfiniBand for three months, ceiling $2.80
|
||||||
|
> per GPU-hour. What have we got?"
|
||||||
|
|
||||||
|
It returns ranked matches, preferring blocks that are sitting idle — those
|
||||||
|
hours are already paid for — and warns explicitly when a match would sell below
|
||||||
|
break-even.
|
||||||
|
|
||||||
|
## Why the surface is small
|
||||||
|
|
||||||
|
Nine tools, each doing one thing. A sprawling tool list measurably degrades
|
||||||
|
model performance, and anything genuinely niche is reachable through
|
||||||
|
`pig_search` or the HTTP API. If you need something that is not here, it is
|
||||||
|
probably better added as a service method than as a tenth tool.
|
||||||
|
|
||||||
|
## What it cannot do
|
||||||
|
|
||||||
|
The MCP server holds an API key and calls the same HTTP API a browser does. It
|
||||||
|
has no database credentials and no privileged path. There is deliberately no
|
||||||
|
tool that provisions infrastructure, spends money, or emails a customer.
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# The ontology
|
||||||
|
|
||||||
|
Why PIG is shaped the way it is. Read `packages/core/src/ontology.ts` alongside
|
||||||
|
this — the code carries the same reasoning in comments, and it is the version
|
||||||
|
that cannot go stale.
|
||||||
|
|
||||||
|
## The one table that matters
|
||||||
|
|
||||||
|
```
|
||||||
|
capacity_commitment ──┐
|
||||||
|
(what we bought, │
|
||||||
|
at a known cost) │
|
||||||
|
├──▶ allocation ──▶ margin, utilisation, idle
|
||||||
|
│ (what we sold,
|
||||||
|
demand_deal ──┘ at a known price)
|
||||||
|
(what we sold)
|
||||||
|
```
|
||||||
|
|
||||||
|
Margin, utilisation and idle capacity all fall out of that single join. No
|
||||||
|
generic CRM can compute any of them, because none has a concept of a
|
||||||
|
cost-bearing commitment sitting behind the pipeline.
|
||||||
|
|
||||||
|
**Cost is charged against the full commitment, not only the hours that sold.**
|
||||||
|
Unsold hours are already paid for. Charging only the allocated share would
|
||||||
|
report a healthy margin on a block that is losing money — precisely the failure
|
||||||
|
this system exists to prevent.
|
||||||
|
|
||||||
|
## Three teams
|
||||||
|
|
||||||
|
**Supply**, **demand**, and **research**. Research is first-class rather than an
|
||||||
|
afterthought: internal research burn is real capacity consumption competing with
|
||||||
|
revenue for the same GPUs, and margin math that cannot see it is wrong.
|
||||||
|
|
||||||
|
## Pipelines
|
||||||
|
|
||||||
|
**Demand** — `qualification → legal → scoping → proposal → procurement → POC →
|
||||||
|
deployment → expansion`. Note that **legal sits second**. Customers do not hand
|
||||||
|
workloads to an infrastructure provider before paper is executed. Most CRMs put
|
||||||
|
contracting at the end of the funnel and are simply wrong about it here.
|
||||||
|
|
||||||
|
**Supply** — `sourced → qualifying → technical diligence → financial diligence →
|
||||||
|
pricing → contracting → onboarding → live → renewal`. Qualification is split in
|
||||||
|
two because accepting capacity is a two-key decision: engineering judges whether
|
||||||
|
the cluster can do the work, finance judges whether the economics clear. Both
|
||||||
|
verdicts are recorded attributably.
|
||||||
|
|
||||||
|
## Capacity is a shape, not a rectangle
|
||||||
|
|
||||||
|
A commitment carries `shape: {intervals[], quantities[]}` — how many GPUs are
|
||||||
|
held during each interval. Real contracts ramp across tranches and step down at
|
||||||
|
checkpoints. A single start/end/total flattens that and then reports
|
||||||
|
availability that does not exist in the month someone wants it.
|
||||||
|
|
||||||
|
Availability at any instant is therefore:
|
||||||
|
|
||||||
|
```
|
||||||
|
available(t) = shapeQuantityAt(t) − Σ overlapping allocations(t)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Holds reserve; they do not sell
|
||||||
|
|
||||||
|
A live hold removes capacity from everyone else's availability — otherwise two
|
||||||
|
sellers promise the same GPUs — but does not count toward utilisation or
|
||||||
|
revenue, because it has not sold. Conflating the two is how a pipeline of
|
||||||
|
optimistic holds comes to look like a full book. Holds expire on a timer so a
|
||||||
|
stalled deal releases inventory automatically.
|
||||||
|
|
||||||
|
## Service levels come in three shapes
|
||||||
|
|
||||||
|
A compute aggregator generally **cannot** offer a conventional uptime guarantee
|
||||||
|
on capacity it resells and does not control, and says so publicly. So `slaKind`
|
||||||
|
distinguishes:
|
||||||
|
|
||||||
|
- `none` — self-serve, no commitment at all
|
||||||
|
- `credits_policy` — a reliability tier plus service credits. **Not** an uptime
|
||||||
|
guarantee, and must never be displayed as one
|
||||||
|
- `negotiated` — a real signed SLA with committed, measurable metrics
|
||||||
|
|
||||||
|
Remedies matter as much as targets. `remedyType` includes `fee_abatement`,
|
||||||
|
where payment obligations are *cancelled* for affected capacity until service is
|
||||||
|
restored — uncapped in duration and materially better than a capped credit. It
|
||||||
|
cannot be expressed as a credit percentage, so it gets its own representation.
|
||||||
|
|
||||||
|
## Export control is a predicate, not a flag
|
||||||
|
|
||||||
|
US controls on advanced computing apply an **ultimate parent** test that reaches
|
||||||
|
through the corporate tree: an entity can be restricted because of where its
|
||||||
|
parent is headquartered, even when the entity itself sits somewhere
|
||||||
|
unrestricted. Country of incorporation is therefore not a valid key.
|
||||||
|
|
||||||
|
Compliance is evaluated **on the allocation edge** — this buyer, this beneficial
|
||||||
|
owner, this physical jurisdiction — recorded with its reasoning and rule
|
||||||
|
version, and re-evaluated on resale or migration. See
|
||||||
|
`packages/db/src/schema/compliance.ts`. PIG records and surfaces; it does not
|
||||||
|
make the legal determination for you.
|
||||||
|
|
||||||
|
## Evidence
|
||||||
|
|
||||||
|
Agent-derived claims land in `facts` with a confidence score, a band, evidence
|
||||||
|
and a source URL. Only `verified` claims self-apply; anything weaker waits for a
|
||||||
|
human. An agent permitted to write unattributed claims will eventually write a
|
||||||
|
wrong one, and nobody will be able to tell which.
|
||||||
|
|
||||||
|
The same principle governs seed data about real people: every record carries a
|
||||||
|
grade and a citation, authorship is never promoted to employment, and no email
|
||||||
|
address is ever inferred.
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Seed data and its provenance
|
||||||
|
|
||||||
|
PIG ships with a roster of publicly documented people so the application is
|
||||||
|
legible on first run. It is public research, not an assertion of fact.
|
||||||
|
|
||||||
|
## Rules applied
|
||||||
|
|
||||||
|
1. **Every record carries a confidence grade and a source URL.** `confirmed`
|
||||||
|
means two or more independent sources; `probable` means one good one;
|
||||||
|
`unverified` means a single weak or self-reported source. The grade is shown
|
||||||
|
in the interface wherever the record appears — a single-source claim about a
|
||||||
|
real person must never look as solid as a corroborated one.
|
||||||
|
|
||||||
|
2. **No email addresses.** None are published by the subjects. Guessing them
|
||||||
|
from a name and a domain is unreliable, and when a guess lands it lands on a
|
||||||
|
real person who did not ask to be contacted.
|
||||||
|
|
||||||
|
3. **Authorship is not employment.** People named on papers or in repositories
|
||||||
|
are recorded with the affiliation actually evidenced — `contributor`,
|
||||||
|
`resident`, `alumni` — never promoted to `staff` to make the roster look
|
||||||
|
fuller.
|
||||||
|
|
||||||
|
4. **"Not found" is recorded, not invented.** Where a name was supplied but
|
||||||
|
could not be sourced, it appears in `UNRESOLVED_NAMES` with a note. Absence
|
||||||
|
is weak evidence: a junior or deliberately non-public employee looks
|
||||||
|
identical to a failed search.
|
||||||
|
|
||||||
|
## Known limitations
|
||||||
|
|
||||||
|
- Sourced August 2026. It will go stale — that is what `sourceUrl` is for.
|
||||||
|
- LinkedIn, Glassdoor and several job boards refuse automated fetching, so some
|
||||||
|
records rest on search-result summaries rather than a page that was read
|
||||||
|
end to end. Those are graded accordingly.
|
||||||
|
- One departure is recorded explicitly (a co-author who now lists a different
|
||||||
|
company) so the roster does not quietly imply current employment.
|
||||||
|
- One name in the original brief could not be tied to the company by any source
|
||||||
|
and is deliberately **not** seeded.
|
||||||
|
|
||||||
|
## Removing yourself
|
||||||
|
|
||||||
|
If you are seeded here and would rather not be, open an issue and the record
|
||||||
|
will be removed. Deleting a contact in the application also removes it
|
||||||
|
permanently.
|
||||||
|
|
||||||
|
## Turning it off
|
||||||
|
|
||||||
|
Seeding is a separate command and is never automatic:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run db:seed # opt in
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip it and PIG starts empty apart from a development user.
|
||||||
Reference in New Issue
Block a user