Rebuild Piggy's interface, and give the demo book a business to describe
Piggy answered in raw markdown, threw away every tool result it streamed, and fought the reader's scroll on every token. The three surfaces that made it worth having — what it read, how it reasoned, what it cost — were all on the wire and none of them reached the screen. The transcript is now composed of five parts under components/piggy: answers render through streamdown, the container sticks to the bottom without pinning the reader there, tool steps say what they read and link to the record, and each turn carries its model and token count. Three lifecycle bugs went with them: Stop left a permanent spinner, a truncated stream was indistinguishable from thinking, and a failed send destroyed the message it failed to send. Underneath, the inference path grew timeouts, jittered retries on 429 and 5xx, tolerance of the malformed frames a 30B model emits, and an agent_runs row per turn so chat spend is observable. The system prompt now states that a field ending in Cents is cents — without it nemotron renders costPerGpuHourCents: 189 as "$189 per GPU-hour", which is a 100x error on the most scrutinised number in the room. The demo book was arithmetically incoherent: every deal's value contradicted its own allocation revenue by up to 3.6x, nothing had ever closed, no customer had any paper, and the marketplace was empty. Deal value is now derived from the allocation, the book clears 5.3% across five blocks with one deliberately underwater, and the renewal, compliance and agent-provenance machinery finally has rows to act on. A --clear that deleted every obligation, SLA term and capacity request in the database regardless of origin is scoped to the demo's own ids. Around that: accounts have a detail page, ⌘K searches the book, Settings can mint the API keys it always claimed to, and deploy.sh actually ships the agent instead of silently skipping its compose profile. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* Demo dataset — a plausible book of business, for development and demos.
|
||||
*
|
||||
* Separate from `../index.ts` (which seeds publicly-sourced, cited people)
|
||||
* because this is **invented**. It exists so the product is legible before
|
||||
* anyone has entered real data: margin that moves, blocks at different
|
||||
* utilisation, deals spread across both pipelines, contracts with real
|
||||
* structure.
|
||||
*
|
||||
* Two integrity rules, and they are not fussiness:
|
||||
*
|
||||
* **Every record is prefixed `DEMO —`.** A screenshot of this must never be
|
||||
* mistakeable for real business.
|
||||
*
|
||||
* **Demand-side customers are fictional.** Suppliers are real companies —
|
||||
* they are public, and naming the actual market is the point — but their
|
||||
* commitments are labelled and the prices are illustrative. Inventing
|
||||
* *customers* with invented contract values against real named companies
|
||||
* would be fabricating commercial records about real businesses, which is a
|
||||
* different thing entirely and not worth the realism.
|
||||
*
|
||||
* Remove it all with `pnpm db:demo -- --clear`.
|
||||
*
|
||||
* The numbers are chosen to teach. The book as a whole clears a modest margin —
|
||||
* roughly what this industry actually earns once capacity cost is charged
|
||||
* honestly — while individual blocks tell different stories:
|
||||
*
|
||||
* the large H200 block carries the book;
|
||||
* the EU H100 block is UNDERWATER at 55% sold, because a 28% markup needs
|
||||
* ~78% sold to break even at all;
|
||||
* the community A100 pool has a large hold that has not converted, so it
|
||||
* shows as reserved-but-unsold — the distinction between "sold" and "held"
|
||||
* made visible rather than theoretical.
|
||||
*
|
||||
* A demo that opens on a healthy total and reveals the problems on drill-down
|
||||
* is more useful than one that opens on a loss, which reads as a broken
|
||||
* product rather than an under-utilised book.
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* This file is the orchestrator and the home of everything the slices share.
|
||||
* The book itself is one module per slice — supply, demand, contracts,
|
||||
* activities, compliance, agent facts, learn, calendar, teardown — because it
|
||||
* was a single 1,400-line file that several people needed to edit at once, and
|
||||
* every edit collided. The order of the inserts below is the order it has
|
||||
* always run in; sections depend on ids the earlier ones return.
|
||||
*/
|
||||
import { quarterBoundsFor } from '@pig/core';
|
||||
import { createDatabase, type Database } from '../../client';
|
||||
import { users } from '../../schema/index';
|
||||
import { seedSupplyActivities } from './activities';
|
||||
import { seedFacts } from './agent';
|
||||
import { seedCalendar } from './calendar';
|
||||
import { seedCompliance } from './compliance';
|
||||
import { seedDemandPaper } from './contracts';
|
||||
import { seedDemand } from './demand';
|
||||
import { seedHostedLearn, seedLearn } from './learn';
|
||||
import { seedSupply } from './supply';
|
||||
|
||||
const db = createDatabase();
|
||||
const PREFIX = 'DEMO — ';
|
||||
|
||||
const day = 86_400_000;
|
||||
const now = Date.now();
|
||||
const at = (days: number) => new Date(now + days * day);
|
||||
|
||||
/**
|
||||
* Dates are placed by QUARTER, and deterministically.
|
||||
*
|
||||
* This file used to scatter close dates with `at(20 + Math.random() * 60)`,
|
||||
* which put the whole book in one arbitrary bucket, differently on every run —
|
||||
* so the quarterly view could not be demonstrated and the CI seed-idempotency
|
||||
* gate was one unlucky reseed away from a false failure. Placement is now
|
||||
* deliberate: something in the quarter just gone, several in the one we are
|
||||
* in, and a couple in the next, so the calendar has all three states to show.
|
||||
*/
|
||||
const thisQuarter = quarterBoundsFor(new Date(now));
|
||||
|
||||
function quarterAt(offset: -1 | 0 | 1, fraction: number): Date {
|
||||
const bounds =
|
||||
offset === 0
|
||||
? thisQuarter
|
||||
: quarterBoundsFor(
|
||||
new Date(
|
||||
offset < 0 ? thisQuarter.from.getTime() - 1 : thisQuarter.to.getTime(),
|
||||
),
|
||||
);
|
||||
const span = bounds.to.getTime() - bounds.from.getTime();
|
||||
return new Date(bounds.from.getTime() + Math.round(span * fraction));
|
||||
}
|
||||
|
||||
/** GPU-hours for a block, allowing for a maintenance/ramp haircut. */
|
||||
const hours = (gpus: number, days: number, efficiency = 0.94) =>
|
||||
String(Math.round(gpus * 24 * days * efficiency));
|
||||
|
||||
/**
|
||||
* What every module of the demo seed is handed, and why it is a parameter
|
||||
* rather than an import.
|
||||
*
|
||||
* The modules under `demo/` deliberately import no *value* from this file. If
|
||||
* they did, ES module evaluation would run their bodies before this one, so a
|
||||
* constant declared at module scope with `${prefix}` in it would read PREFIX
|
||||
* before it was initialised and the whole seed would die in the temporal dead
|
||||
* zone — a failure that would appear only when someone added an innocent
|
||||
* top-level constant to one of the slices. Passing the shared pieces down makes
|
||||
* that impossible to reintroduce, whichever module a later change lands in.
|
||||
*/
|
||||
export interface DemoContext {
|
||||
readonly db: Database;
|
||||
/** `DEMO — `. Every invented record carries it; `clear()` matches on it. */
|
||||
readonly prefix: string;
|
||||
/** A date relative to the instant the seed started, fixed for the whole run. */
|
||||
readonly at: (days: number) => Date;
|
||||
/** GPU-hours for a block, allowing for a maintenance/ramp haircut. */
|
||||
readonly hours: (gpus: number, days: number, efficiency?: number) => string;
|
||||
/** A point inside the previous, current or next quarter. See `quarterAt`. */
|
||||
readonly quarterAt: (offset: -1 | 0 | 1, fraction: number) => Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* The one context the commands run against, and therefore the one connection
|
||||
* pool. Built here rather than per module so that importing two slices cannot
|
||||
* open two pools against the same database.
|
||||
*/
|
||||
export const demoContext: DemoContext = { db, prefix: PREFIX, at, hours, quarterAt };
|
||||
|
||||
export async function seedDemo(context: DemoContext): Promise<void> {
|
||||
console.log('Seeding the demo book…\n');
|
||||
|
||||
// The id map every later section joins against: one capacity commitment per
|
||||
// supplier domain, which is how the demand book names the block it draws on.
|
||||
const commitmentIds = await seedSupply(context);
|
||||
|
||||
await seedDemand(context, commitmentIds);
|
||||
|
||||
// Customer paper is written after the demand book, because it reads the
|
||||
// accounts, deals and allocations that pass creates. Without it the renewal
|
||||
// signals the Growth page is built around have nothing to fire on.
|
||||
await seedDemandPaper(context);
|
||||
|
||||
const compliance = await seedCompliance(context);
|
||||
|
||||
// The supply-side timeline, the closed deals' histories, and the sweep that
|
||||
// restamps lastActivityAt from the activities themselves. It must run after
|
||||
// every section that creates an account or a deal, because it both rescues
|
||||
// records that would otherwise have an empty timeline and restamps
|
||||
// lastActivityAt from what it can see. Compliance creates three demand
|
||||
// accounts of its own, so running before it left those three with no history
|
||||
// at all on a first run — and a second run of the demo seed then added the
|
||||
// missing 21 activities, which is why the book only settled after two runs.
|
||||
// Compliance neither reads nor writes activities, so nothing moves the other
|
||||
// way.
|
||||
await seedSupplyActivities(context);
|
||||
|
||||
// Resolved once, here, because the calendar and the learn library must agree
|
||||
// on who owns their rows; two lookups would be two chances to disagree.
|
||||
// Ordered by creation, not arbitrary: the demand book now seeds its own
|
||||
// sellers, and an unordered limit(1) would hand the calendar and the learn
|
||||
// library to whichever of them Postgres returned first.
|
||||
const [owner] = await context.db
|
||||
.select({ id: users.id })
|
||||
.from(users)
|
||||
.orderBy(users.createdAt)
|
||||
.limit(1);
|
||||
const ownerUserId = owner?.id ?? null;
|
||||
|
||||
const calendar = await seedCalendar(context, ownerUserId);
|
||||
const learn = await seedLearn(context, ownerUserId);
|
||||
|
||||
// The self-hosted set, which is real rather than invented — see the long
|
||||
// note on `seedHostedLearn`. Folded into the demo seed so one command gives
|
||||
// a complete Learn page, but kept in its own function with its own flags
|
||||
// because it is not demo data and must not be removed with `--clear`.
|
||||
const hosted = await seedHostedLearn(context);
|
||||
|
||||
const facts = await seedFacts(context);
|
||||
|
||||
console.log(' 5 capacity commitments (4 live, 1 lapsed), with sites, MSAs and negotiated SLAs');
|
||||
console.log(` ${facts.total} agent-derived facts (${facts.added} new) — 2 applied, 4 awaiting review`);
|
||||
console.log(
|
||||
` ${facts.tasks} agent tasks and ${facts.runs} Piggy runs, ${facts.actions} idempotency-keyed actions, ` +
|
||||
`${(facts.costMicroCents / 1_000_000).toFixed(4)} cents of model spend`,
|
||||
);
|
||||
console.log(
|
||||
' 12 demand deals across all ten stages — 2 won, 1 lost, 1 expansion off a closed parent — and 8 supply deals',
|
||||
);
|
||||
console.log(' Allocations including one unconverted hold and internal research burn');
|
||||
console.log(
|
||||
' Close dates placed deliberately in the previous, current and next quarter',
|
||||
);
|
||||
console.log(
|
||||
` ${compliance.authorizations.total} export authorisations (${compliance.authorizations.added} new) — ` +
|
||||
`one lapsed, one expiring this quarter, ${compliance.artifacts.total} compliance artefacts, ` +
|
||||
`${compliance.decisions.total} export-control decisions (allow, needs_review, block), ` +
|
||||
`${calendar.total} calendar entries (${calendar.added} new)`,
|
||||
);
|
||||
console.log(
|
||||
` ${learn.total} illustrative concept videos (${learn.added} new), members-only`,
|
||||
);
|
||||
console.log(
|
||||
` ${hosted.present} PIG-hosted learn videos (${hosted.added} new)` +
|
||||
`${hosted.missing > 0 ? `, ${hosted.missing} manifest entries with no file yet` : ''}`,
|
||||
);
|
||||
console.log('\nEverything is prefixed "DEMO — ". Remove it with: pnpm db:demo -- --clear');
|
||||
console.log('The PIG-hosted rows are NOT prefixed and survive that. Remove them with: pnpm db:demo -- --clear-hosted');
|
||||
}
|
||||
|
||||
export { clear } from './clear';
|
||||
export { HOSTED_LEARN_MANIFEST, clearHostedLearn, seedHostedLearn } from './learn';
|
||||
Reference in New Issue
Block a user