Shoot the README screenshots, and make them regenerable

The screenshot section had been a placeholder since the shell was rebuilt as
three panes, because there was no cheap way to re-shoot and a stale image is
worse than no image. So this ships the capture, not just the captures:
scripts/screenshots.mjs takes all ten pages at 1440x900 and 393x852, in light
and dark, and screenshots-encode.py halves and re-encodes them to WebP — 16MB
of PNG becomes 1.9MB in the tree.

Two things would silently ruin a run, and the script exists to encode both.
Seeding localStorage['pig.themeMode'] is not enough: the appearance preference
is authoritative server-side and adopted after hydration, so every dark capture
snapped back to light a beat after first paint. The /api/me/profile response is
rewritten instead. And pig.sidebarOpen / pig.piggyDockOpen are per-device, so
whatever the last human left behind would otherwise leak in. The run also fails
on a wrong theme, a horizontal scrollbar or a console error — three things a
screenshot cannot show you.

Piggy is not pictured mid-conversation. It is off by default and no inference
credential exists, so such an image would be a staged transcript rather than a
capture. The README says that rather than implying the feature is missing.

Correcting what the README asserted while shooting against the running code:
read authorisation IS enforced — createReadGuardRoutes is mounted ahead of the
feature routes, and routes/learn.ts and routes/activities.ts are mounted too,
so only the HubSpot pair is still unreachable. The remaining read gap is that a
grant cannot be narrowed, there being no row-level team filter in the query
layer. Counts refreshed against the tree: 275 tests, ~47k lines, 14 migrations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-13 21:15:45 -07:00
parent 9db53cb36f
commit 10d79fc35d
44 changed files with 563 additions and 33 deletions
+118 -33
View File
@@ -60,18 +60,102 @@ for, not because they are universal.
## Screenshots
> **Placeholder — fresh captures needed.** The application shell was rebuilt as
> a three-pane layout (header, collapsible sidebar rail, docked Piggy) and
> every screenshot taken before that redesign now misrepresents the product.
> Rather than ship misleading images, this section is deliberately empty until
> the new shell is re-shot at desktop and at 393px, in light and dark.
>
> Pages to capture: `/` Overview, `/margin`, `/capacity` (the matcher),
> `/calendar`, `/piggy` docked beside a record.
Captured against the current shell — header, collapsible sidebar rail, docked
Piggy — running locally on the seed plus demo book (`db:seed` and `db:demo`), so
every number below is computed by the code in this repository rather than drawn.
Records prefixed `DEMO —` are fictional; the rest are the sourced, cited seed.
<!-- TODO(screenshots): add docs/screenshots/{overview,margin,capacity-match,calendar}.png
— post-redesign, light and dark, desktop and 393px. Do not reuse the
pre-redesign pig-*.png audit captures. -->
Each image follows your own system theme. Both themes are shown explicitly
further down, and [the full gallery](docs/screenshots.md) has all ten pages at
1440px and 393px, in light and dark. Desktop captures are the 1440×900 viewport
rather than the full scroll height — what you see is what fits above the fold.
`node scripts/screenshots.mjs` re-shoots the set.
**Overview** — margin, sold ratio and idle capacity across the book, with the
commitments you are paying for and not selling ranked by cost exposure.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/overview-desktop-dark.webp">
<img src="docs/screenshots/overview-desktop-light.webp" alt="PIG Overview: gross margin $675,871.37, 79.1% sold ratio, 1.3M idle GPU-hours, and a ranked list of capacity bought and unsold.">
</picture>
**Margin** — revenue from what was sold against the *full* cost of what was
bought, per commitment. `Cost covered` and a break-even price are the two states
that matter; charging only the allocated share of cost would report a healthy
margin on a block that is losing money.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/margin-desktop-dark.webp">
<img src="docs/screenshots/margin-desktop-light.webp" alt="PIG Margin: revenue $12.4M against $11.7M of full committed cost, broken down by commitment with sold ratio, cost per hour and break-even price.">
</picture>
**Capacity → Match a requirement** — the matcher. Ask what a customer needs and
PIG scores it against capacity already under commitment, saying why each block
fits, and hands you straight to the allocation that records the sale.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/capacity-match-desktop-dark.webp">
<img src="docs/screenshots/capacity-match-desktop-light.webp" alt="PIG capacity matcher: a requirement for 64 H100_80GB with high-speed interconnect, scored against two commitments at 64% and 58% fit with an Allocate this capacity action on each.">
</picture>
**Growth** — deterministic attention scores over customer paper, deal activity
and sold or reserved capacity. Every point is an explained signal with its
sources named; nothing here is a model's guess at a win probability.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/growth-desktop-dark.webp">
<img src="docs/screenshots/growth-desktop-light.webp" alt="PIG Growth: accounts ranked by attention score, each tagged deployed, expansion candidate, at risk or coverage gap, with the scoring signals listed underneath.">
</picture>
**Calendar** — what closes, what renews, what expires and when capacity lands,
projected from the records that already carry the dates. Export authorisations
expire on this timeline too, because an expired one converts lawful business
into unlawful business.
<picture>
<source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/calendar-desktop-dark.webp">
<img src="docs/screenshots/calendar-desktop-light.webp" alt="PIG Calendar for 2026-Q3: weighted pipeline, deals closing, renewals, obligations due and authorisations expiring, above a quarter timeline with one lane per kind.">
</picture>
### Light and dark
Theme is a stored preference that follows a person between devices, resolved
before first paint by an inline script so dark-mode users never get a white
flash. Both tunings of the accent palette are defined in `@pig/core` and applied
as CSS variables at runtime, so there is one definition of each colour.
The demand pipeline, in both. Legal sits second rather than last, because MSA
and DPA execution gates delivery rather than closing the deal — most CRMs put
contracts at the end of the funnel and are wrong about it for this market.
*Light:*
<img src="docs/screenshots/demand-desktop-light.webp" alt="PIG demand pipeline in light mode: ten stages from qualification through legal, scoping, proposal, procurement, POC and deployment, with deal cards showing ACV, product line and MSA/DPA badges.">
*Dark:*
<img src="docs/screenshots/demand-desktop-dark.webp" alt="The same demand pipeline in dark mode.">
Side by side at 393px, where both tunings have to survive a smaller surface:
| Supply pipeline · light | Supply pipeline · dark |
| --- | --- |
| <img src="docs/screenshots/supply-mobile-light.webp" alt="Supply pipeline at 393px in light mode"> | <img src="docs/screenshots/supply-mobile-dark.webp" alt="Supply pipeline at 393px in dark mode"> |
### Mobile
PIG is responsive to 393px — the sidebar becomes a bottom tab bar, tables become
cards, and the safe-area insets are handled. It is not a native app.
| Overview · light | Overview · dark | Margin · dark |
| --- | --- | --- |
| <img src="docs/screenshots/overview-mobile-light.webp" alt="PIG Overview at 393px in light mode, with a bottom tab bar"> | <img src="docs/screenshots/overview-mobile-dark.webp" alt="PIG Overview at 393px in dark mode"> | <img src="docs/screenshots/margin-mobile-dark.webp" alt="PIG Margin at 393px in dark mode, the commitment table reflowed into cards"> |
Piggy is deliberately not pictured mid-conversation. It is off by default
(`PIGGY_ENABLED=false`, and the Compose service sits behind a profile), and
showing it answering would mean staging a transcript rather than capturing one.
What it may and may not do is described under [the agent
surface](#the-agent-surface).
## The two-sided data model
@@ -245,7 +329,7 @@ half-working.
## Architecture
A pnpm monorepo. Around 45k lines of TypeScript including tests, 261 tests
A pnpm monorepo. Around 47k lines of TypeScript including tests, 275 tests
across five packages, green CI.
```
@@ -257,9 +341,9 @@ apps/
cli/ `pig`, the HTTP surface for scripts and agent kernels
packages/
core/ Ontology, permissions, margin arithmetic, palette — no I/O
db/ Drizzle schema (47 tables), 13 migrations, seed and demo data
db/ Drizzle schema (47 tables), 14 migrations, seed and demo data
prime/ Typed client for the Prime Intellect compute API
docs/ ontology.md, build-plan.md, agents.md, seed-data.md
docs/ ontology.md, screenshots.md, build-plan.md, agents.md, seed-data.md
deploy/ Caddyfile example, autodeploy units, deployment notes
```
@@ -322,11 +406,15 @@ required to hold it is what separates the roster from the cost book.
`economics:read` is the one that matters — supplier cost per GPU-hour and
break-even price *are* the business.
⚠️ **The read half is written and tested but not yet enforced.** The policy
table (`apps/api/src/routes/read-guards.ts`) and its middleware exist, and
`read-governance.test.ts` fails when a GET appears that no rule covers — but
`createReadGuardRoutes` is not mounted in `app.ts`, so today every
authenticated member can read the whole book including cost. See
The read half is enforced. The policy table lives in
`apps/api/src/routes/read-guards.ts` and `createReadGuardRoutes` is mounted in
`app.ts` **before** the feature routes — Hono runs matched handlers in
registration order, so a guard registered after its route would return 200 while
looking correct. `read-governance.test.ts` pins that ordering in both
directions, and fails when a GET appears that no rule covers, so a new read
endpoint cannot ship ungoverned by accident.
What it still cannot do is filter *within* a grant: see
[limitations](#what-is-not-built-yet).
## The agent surface
@@ -399,7 +487,7 @@ see [docs/agents.md](./docs/agents.md).
CI is Gitea Actions, one sequence, about two minutes. It typechecks every
package, applies the migration chain **twice** to a real empty Postgres, asserts
the seed is idempotent, runs 261 unit tests and the critical-path E2E, boots the
the seed is idempotent, runs 275 unit tests and the critical-path E2E, boots the
server and curls it, builds the front end, checks the inline theme script still
hashes to the value the proxy's CSP allows, and builds the Docker image.
@@ -428,20 +516,16 @@ one thing an on-call needs at 04:00.
Said plainly, because you are going to grep the repo anyway.
**Read authorisation is not enforced.** As above: the policy, the middleware
and the governance test all exist; the router line that mounts them does not.
Any authenticated member can currently read cost and margin regardless of team
or role. This is the most significant gap in the product and it is one line in
`app.ts`.
**Read authorisation is enforced, but only at the grant.** A capability is held
or it is not. Once `economics:read` is held, it returns every commitment's cost
— there is no filter that narrows it to one team's book, because no row-level
team filter exists anywhere in the query layer. That is the gap to close before
PIG serves a company where "supply can see supply's costs" is a requirement.
**Four route modules are written, tested and never mounted.**
`routes/read-guards.ts`, `routes/learn.ts`, `routes/hubspot.ts` and
`routes/hubspot-webhook.ts` are all absent from `app.ts`. Consequences: the
**Learn** page is in the navigation and its API answers 404, and the HubSpot
integration — OAuth, connections, sync jobs, webhook verification, seven
`hubspot_*` tables — is unreachable. `routes/activities.ts` is likewise
unmounted, but harmlessly: an older inline `POST /api/activities` in `app.ts`
still serves it.
**The HubSpot integration is written, tested and never mounted.**
`routes/hubspot.ts` and `routes/hubspot-webhook.ts` are both absent from
`app.ts`, so OAuth, connections, sync jobs, webhook verification and seven
`hubspot_*` tables are all unreachable from the running server.
**Six of the eight declared agent task kinds are never enqueued.** The worker
is complete and generic, but only `enrich_account` and `enrich_contact` are
@@ -478,6 +562,7 @@ responsive to 393px; it is not a native app.
- **[AGENTS.md](./AGENTS.md) — start here if you are joining this codebase.**
Architecture rules, the traps that have already bitten, and conventions.
- [Screenshots](./docs/screenshots.md) — every page, at 1440px and 393px, light and dark
- [Ontology](./docs/ontology.md) — the domain model, and why it is shaped this way
- [Build plan](./docs/build-plan.md) — what shipped, what remains, in dependency order
- [Agent integration](./docs/agents.md) — MCP clients and the CLI