666310b264
The Motion half of the demo book was two engagements, which was enough to show that the loop works and not enough to show what the product is for. The page that matters asks whether the motion is repeating, and a stage rail of zeroes cannot answer it. Eight engagements now, one at every open stage, hanging off demand deals the demand book already creates — the pipeline happened to have exactly one open deal at each of the eight, so no deal was invented and the quoted pipeline counts are unchanged. Forty-four artefacts, nineteen scores, one promotion. Three things the book is laid out to prove that a folder of templates cannot. Every stage is occupied, and every one of the twelve starter templates is instantiated at least once, so "stages covered 8/8" is a measurement rather than a claim about the seed. Scores move, and sometimes move down. Nineteen scores across eight trajectories, with nine dimensions regressing somewhere — the Verity fine-tuning record runs 62.5 -> 60.5 -> 84.0 -> 80.8, because a scorecard that only ever rises is a ratchet and teaches a reader to distrust it. Every score is computed with `motionScoreBasisPoints` and banded with `motionBand` rather than written as a literal, so the seed and the product cannot disagree about what the same dimensions are worth. The artefact bodies are the customer's own facts — named people, real volumes, the specific thing going wrong, and a live unresolved risk in each. An artefact whose body is the template with the blanks still in it is precisely what this data exists to disprove. Six of the forty-four have no template at all, which is the honest shape of an engagement and the reason `kind` is carried on the artefact rather than derived: `engagement_artifacts.kind` is NOT NULL and a derived kind would have been null for exactly those six. The bodies live in JSON beside the loader for the same reason the starter library's do — forty-four markdown bodies as backtick strings is a module nobody can review. The promotion copies `body` from the artefact verbatim, as `promoteArtifact` does, rather than writing a hand-authored version 2. A demo that produced a row the real path could not have produced would teach the wrong shape of the table. Two name collisions the authors could not see are fixed: a Quillon contact shared a full name with a demo seller, and an Aurelian one shared a surname with another. `usage_count` is raised once per template rather than once per artefact, so it stays symmetrical with the decrement `clear()` already does — verified by tearing the book down and confirming the library returns to twelve templates with every counter back at zero, since a counter left above zero makes a starter template permanently un-editable.
248 lines
12 KiB
TypeScript
248 lines
12 KiB
TypeScript
/**
|
|
* 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<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);
|
|
|
|
// 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<number>`(select count(*)::int from ${capacityCommitments})`,
|
|
demandDeals: sql<number>`(select count(*)::int from ${demandDeals})`,
|
|
supplyDeals: sql<number>`(select count(*)::int from ${supplyDeals})`,
|
|
demandStages: sql<number>`(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';
|