/** * 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 { sql } from 'drizzle-orm'; import { createDatabase, type Database } from '../../client'; import { capacityCommitments, demandDeals, supplyDeals, 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 { seedMotionEngagements } from './motion'; 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 { 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); // After the demand book, which owns the deals both engagements hang off, and // after the base seed has put the starter library in place — an engagement // whose artifacts came from nothing would demonstrate the folder rather than // the loop. Run `pnpm db:seed` before `pnpm db:demo`, as the README says. const motion = await seedMotionEngagements(context); const facts = await seedFacts(context); /* * Counted, not asserted. These lines were hardcoded when the book was split * into modules, and they drifted the moment the seed grew: the summary * claimed 12 demand deals and 5 commitments against a database holding 13 * and 6. A seed that misreports what it wrote teaches an operator to * distrust the only feedback the command gives them. */ const [written] = await context.db .select({ commitments: sql`(select count(*)::int from ${capacityCommitments})`, demandDeals: sql`(select count(*)::int from ${demandDeals})`, supplyDeals: sql`(select count(*)::int from ${supplyDeals})`, demandStages: sql`(select count(distinct stage)::int from ${demandDeals})`, }) .from(sql`(select 1) as one`); const book = written ?? { commitments: 0, demandDeals: 0, supplyDeals: 0, demandStages: 0 }; console.log(` ${book.commitments} capacity commitments, 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( ` ${book.demandDeals} demand deals across ${book.demandStages} stages — including a won parent with its ` + `expansion child, and a loss with a reason — and ${book.supplyDeals} 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( ` ${motion.engagements} Motion engagement(s) with ${motion.artifacts} artefact(s) and ` + `${motion.scores} qualification score(s) — ${motion.promoted} artefact promoted back into ` + 'the library as a version 2', ); if (motion.missingDeals.length > 0) { console.warn( ` ${motion.missingDeals.length} Motion engagement(s) had no deal to hang off and were ` + `SKIPPED: ${motion.missingDeals.join(', ')}. Run \`pnpm db:demo -- --clear\` and reseed ` + 'to rebuild the demand book.', ); } 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';