Add Motion: the go-to-market operating system on top of the ledger
The ledger answers which contracted capacity is sold, to whom, at what margin. It says nothing about the motion — the repeatable practice that turns a customer conversation into a scoped deployment, and turns that deployment into something the next one reuses. Motion is deliberately not a parallel entity tree. DEMAND_STAGES already is the motion, so Motion binds reusable artefacts to the stages of a demand deal that already exists: an engagement hangs off one deal, cascade deleted, one per deal by unique constraint. Nine closed kinds, each declaring which stages it serves, and a starter library of twelve templates covering all eight open stages. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
* situation this command is for — someone demoing on top of their own data —
|
||||
* and it must leave that data untouched.
|
||||
*/
|
||||
import { eq, inArray, like } from 'drizzle-orm';
|
||||
import { eq, inArray, like, sql } from 'drizzle-orm';
|
||||
import { unstampDemoActivity } from './activities';
|
||||
import {
|
||||
accounts,
|
||||
@@ -21,8 +21,12 @@ import {
|
||||
contractObligations,
|
||||
contracts,
|
||||
demandDeals,
|
||||
engagementArtifacts,
|
||||
engagements,
|
||||
exportAuthorizations,
|
||||
learnResources,
|
||||
motionTemplates,
|
||||
qualificationScores,
|
||||
slaTerms,
|
||||
teamMemberships,
|
||||
users,
|
||||
@@ -79,6 +83,68 @@ export async function clear(context: DemoContext): Promise<void> {
|
||||
if (demandDealIds.length > 0) {
|
||||
await db.delete(capacityRequests).where(inArray(capacityRequests.demandDealId, demandDealIds));
|
||||
}
|
||||
/*
|
||||
* The Motion rows, before the deals they hang off.
|
||||
*
|
||||
* `engagements.demand_deal_id` cascades, so the delete below would take the
|
||||
* engagements, their artefacts and their scores with it — but not the
|
||||
* promoted version 2, which is a `motion_templates` row and would be left
|
||||
* behind as an orphaned version of a shipped lineage with its
|
||||
* `origin_artifact_id` quietly set to null. So the promoted template goes
|
||||
* first, and the three engagement tables are removed explicitly rather than
|
||||
* by cascade, in child-to-parent order, so the teardown reads as what it
|
||||
* does. The starter library itself is NOT touched: it is authored product
|
||||
* content from the base seed, carries no prefix, and survives `--clear` the
|
||||
* same way the PIG-hosted learn rows do.
|
||||
*/
|
||||
const demoEngagements = await db
|
||||
.select({ id: engagements.id })
|
||||
.from(engagements)
|
||||
.where(like(engagements.summary, `${prefix}%`));
|
||||
const engagementIds = demoEngagements.map((engagement) => engagement.id);
|
||||
if (engagementIds.length > 0) {
|
||||
/*
|
||||
* Give back the `usage_count` the demo book borrowed, before the artefacts
|
||||
* that justify it are deleted.
|
||||
*
|
||||
* Above zero, `usage_count` makes a template permanently un-editable — the
|
||||
* API answers 409 and tells you to cut a new version. So a teardown that
|
||||
* left the counter raised would hand back a starter library three of whose
|
||||
* templates can never be edited again, for engagements that no longer
|
||||
* exist, and nothing in the UI would explain why. Decremented by exactly
|
||||
* what the demo added rather than reset to zero, because a real
|
||||
* instantiation on the same template must survive `--clear`.
|
||||
*/
|
||||
const borrowed = await db
|
||||
.select({ templateId: engagementArtifacts.templateId })
|
||||
.from(engagementArtifacts)
|
||||
.where(inArray(engagementArtifacts.engagementId, engagementIds));
|
||||
const usageByTemplate = new Map<string, number>();
|
||||
for (const row of borrowed) {
|
||||
if (row.templateId) {
|
||||
usageByTemplate.set(row.templateId, (usageByTemplate.get(row.templateId) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
for (const [templateId, count] of usageByTemplate) {
|
||||
await db
|
||||
.update(motionTemplates)
|
||||
.set({ usageCount: sql`greatest(${motionTemplates.usageCount} - ${count}, 0)` })
|
||||
.where(eq(motionTemplates.id, templateId));
|
||||
}
|
||||
|
||||
await db
|
||||
.delete(qualificationScores)
|
||||
.where(inArray(qualificationScores.engagementId, engagementIds));
|
||||
await db
|
||||
.delete(engagementArtifacts)
|
||||
.where(inArray(engagementArtifacts.engagementId, engagementIds));
|
||||
await db.delete(engagements).where(inArray(engagements.id, engagementIds));
|
||||
}
|
||||
// After the artefacts, so `origin_artifact_id` is already gone rather than
|
||||
// being set null on the way past. Scoped to the prefix, which the seeded
|
||||
// library deliberately does not carry.
|
||||
await db.delete(motionTemplates).where(like(motionTemplates.title, `${prefix}%`));
|
||||
|
||||
await db.delete(demandDeals).where(like(demandDeals.name, `${prefix}%`));
|
||||
await db.delete(supplyDeals).where(like(supplyDeals.name, `${prefix}%`));
|
||||
await db.delete(capacityCommitments).where(like(capacityCommitments.name, `${prefix}%`));
|
||||
|
||||
@@ -55,6 +55,7 @@ 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();
|
||||
@@ -173,6 +174,12 @@ export async function seedDemo(context: DemoContext): Promise<void> {
|
||||
// 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);
|
||||
|
||||
console.log(' 5 capacity commitments (4 live, 1 lapsed), with sites, MSAs and negotiated SLAs');
|
||||
@@ -197,6 +204,11 @@ export async function seedDemo(context: DemoContext): Promise<void> {
|
||||
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',
|
||||
);
|
||||
console.log(
|
||||
` ${hosted.present} PIG-hosted learn videos (${hosted.added} new)` +
|
||||
`${hosted.missing > 0 ? `, ${hosted.missing} manifest entries with no file yet` : ''}`,
|
||||
|
||||
@@ -0,0 +1,472 @@
|
||||
/**
|
||||
* Two demo engagements, chosen so the loop is visible without a live customer.
|
||||
*
|
||||
* The loop is the only reason Motion exists:
|
||||
*
|
||||
* library template --instantiate--> engagement artifact --promote--> v2
|
||||
*
|
||||
* A screenshot of the library alone shows a folder of documents, which is not
|
||||
* the claim. So one engagement is **mid-POC with three qualification scores**,
|
||||
* because a single score is a number and three are a trajectory — the movement
|
||||
* is the evidence that qualification happened at all — and the other is
|
||||
* **closed-won with a promoted artifact**, which puts a version 2 into the
|
||||
* library carrying `supersedes_id` back to the shipped version 1 and
|
||||
* `origin_artifact_id` back to the engagement that proved it. Both halves of
|
||||
* the FK cycle are therefore populated on a first run.
|
||||
*
|
||||
* Both hang off demand deals the demand book already created, rather than deals
|
||||
* of their own: an engagement has no independent existence, and inventing a
|
||||
* deal here would put a thirteenth row on a pipeline whose counts are quoted in
|
||||
* `demo/index.ts`.
|
||||
*
|
||||
* The scores are computed with `motionScoreBasisPoints` rather than written as
|
||||
* literals. Hard-coding 6600 would let the seed and the product disagree about
|
||||
* what the same dimensions are worth, and the first place anyone would notice
|
||||
* is a demo.
|
||||
*/
|
||||
import { motionBand, motionScoreBasisPoints, type MotionDimensionScore } from '@pig/core';
|
||||
import { and, eq, like, sql } from 'drizzle-orm';
|
||||
import {
|
||||
demandDeals,
|
||||
engagementArtifacts,
|
||||
engagements,
|
||||
motionTemplates,
|
||||
qualificationScores,
|
||||
users,
|
||||
} from '../../schema/index';
|
||||
import type { DemoContext } from './index';
|
||||
|
||||
/**
|
||||
* The scorecard's dimensions and their authored weights, copied from
|
||||
* `seed/motion/trainability-qualification.json`.
|
||||
*
|
||||
* Copied rather than read out of the seeded row, deliberately: a stored score
|
||||
* is a snapshot of the framework *as it was scored*, which is the whole reason
|
||||
* `qualification_scores.dimensions` holds the weights rather than a reference.
|
||||
* Reading them live would make the demo history silently re-weight itself the
|
||||
* day someone edits the template, which is precisely the behaviour the column
|
||||
* exists to prevent.
|
||||
*/
|
||||
const SCORECARD_WEIGHTS: readonly { readonly id: string; readonly weight: number }[] = [
|
||||
{ id: 'task_definability', weight: 10 },
|
||||
{ id: 'verifier_quality', weight: 13 },
|
||||
{ id: 'trace_data_rights', weight: 8 },
|
||||
{ id: 'baseline_measured', weight: 8 },
|
||||
{ id: 'headroom', weight: 10 },
|
||||
{ id: 'env_constructibility', weight: 8 },
|
||||
{ id: 'metric_owner', weight: 9 },
|
||||
{ id: 'budget_source', weight: 9 },
|
||||
{ id: 'exec_sponsor', weight: 4 },
|
||||
{ id: 'security_legal_path', weight: 8 },
|
||||
{ id: 'forcing_function', weight: 8 },
|
||||
{ id: 'expansion_surface', weight: 5 },
|
||||
];
|
||||
|
||||
/**
|
||||
* Three passes over the same scorecard, in the order they were run.
|
||||
*
|
||||
* The trajectory is the teaching: the deal opens borderline on an unmeasured
|
||||
* verifier and no named budget, the POC scoping call fixes the verifier, and
|
||||
* the third pass lands once finance names a source. Two dimensions deliberately
|
||||
* do NOT move — `trace_data_rights` and `expansion_surface` — because a
|
||||
* scorecard where every number rises on every pass is a scorecard nobody is
|
||||
* really filling in.
|
||||
*/
|
||||
const SCORE_PASSES: readonly {
|
||||
readonly daysAgo: number;
|
||||
readonly note: string;
|
||||
readonly scores: Readonly<Record<string, number>>;
|
||||
}[] = [
|
||||
{
|
||||
daysAgo: 52,
|
||||
note:
|
||||
'First pass, straight out of technical discovery. The task is well specified and there ' +
|
||||
'is real headroom, but the verifier is a rubric nobody has run against human labels and ' +
|
||||
'no budget line has been named. Not yet — and the two things to fix are explicit.',
|
||||
scores: {
|
||||
task_definability: 3,
|
||||
verifier_quality: 1,
|
||||
trace_data_rights: 2,
|
||||
baseline_measured: 2,
|
||||
headroom: 3,
|
||||
env_constructibility: 2,
|
||||
metric_owner: 2,
|
||||
budget_source: 1,
|
||||
exec_sponsor: 2,
|
||||
security_legal_path: 2,
|
||||
forcing_function: 2,
|
||||
expansion_surface: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
daysAgo: 27,
|
||||
note:
|
||||
'Re-scored after the POC scoping call. They ran the judge against 300 human-labelled ' +
|
||||
'items and the baseline is now measured rather than remembered. Budget is still a ' +
|
||||
'reallocation nobody has signed.',
|
||||
scores: {
|
||||
task_definability: 3,
|
||||
verifier_quality: 3,
|
||||
trace_data_rights: 2,
|
||||
baseline_measured: 3,
|
||||
headroom: 3,
|
||||
env_constructibility: 3,
|
||||
metric_owner: 3,
|
||||
budget_source: 1,
|
||||
exec_sponsor: 2,
|
||||
security_legal_path: 3,
|
||||
forcing_function: 2,
|
||||
expansion_surface: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
daysAgo: 6,
|
||||
note:
|
||||
'Third pass, in POC week two. Finance named the source and the VP Engineering owns the ' +
|
||||
'metric in their own planning doc. Data rights are unchanged: the customer still cannot ' +
|
||||
'export traces beyond the pilot without a DPA amendment, and that is the live risk.',
|
||||
scores: {
|
||||
task_definability: 4,
|
||||
verifier_quality: 3,
|
||||
trace_data_rights: 2,
|
||||
baseline_measured: 3,
|
||||
headroom: 4,
|
||||
env_constructibility: 3,
|
||||
metric_owner: 4,
|
||||
budget_source: 3,
|
||||
exec_sponsor: 3,
|
||||
security_legal_path: 3,
|
||||
forcing_function: 3,
|
||||
expansion_surface: 3,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function dimensionsFor(scores: Readonly<Record<string, number>>): MotionDimensionScore[] {
|
||||
return SCORECARD_WEIGHTS.map((dimension) => ({
|
||||
id: dimension.id,
|
||||
weight: dimension.weight,
|
||||
score: scores[dimension.id] ?? 0,
|
||||
}));
|
||||
}
|
||||
|
||||
async function findDeal(context: DemoContext, name: string): Promise<string | undefined> {
|
||||
const [deal] = await context.db
|
||||
.select({ id: demandDeals.id })
|
||||
.from(demandDeals)
|
||||
.where(eq(demandDeals.name, name))
|
||||
.limit(1);
|
||||
return deal?.id;
|
||||
}
|
||||
|
||||
async function findSeller(context: DemoContext, name: string): Promise<string | null> {
|
||||
const [seller] = await context.db
|
||||
.select({ id: users.id })
|
||||
.from(users)
|
||||
.where(eq(users.name, `${context.prefix}${name}`))
|
||||
.limit(1);
|
||||
return seller?.id ?? null;
|
||||
}
|
||||
|
||||
async function findTemplate(
|
||||
context: DemoContext,
|
||||
slug: string,
|
||||
): Promise<{ id: string; version: number } | undefined> {
|
||||
const [template] = await context.db
|
||||
.select({ id: motionTemplates.id, version: motionTemplates.version })
|
||||
.from(motionTemplates)
|
||||
.where(and(eq(motionTemplates.slug, slug), eq(motionTemplates.isSystem, true)))
|
||||
.limit(1);
|
||||
return template;
|
||||
}
|
||||
|
||||
export async function seedMotionEngagements(
|
||||
context: DemoContext,
|
||||
): Promise<{ engagements: number; artifacts: number; scores: number; promoted: number }> {
|
||||
const { db, prefix, at } = context;
|
||||
|
||||
/*
|
||||
* The whole section is skipped if either engagement already exists.
|
||||
*
|
||||
* `engagements_demand_deal_key` would refuse a duplicate engagement, but
|
||||
* nothing would refuse a second set of artifacts or — worse — a second
|
||||
* promotion, and a second promotion writes case-study-frame v3 on top of the
|
||||
* v2 this seed already created. `qualification_scores` is append-only by
|
||||
* design and so has no constraint to conflict on at all; three more rows on
|
||||
* every run would turn the trajectory into noise. An existence check on the
|
||||
* parent is the only thing that covers all three.
|
||||
*/
|
||||
const existing = await db
|
||||
.select({ id: engagements.id })
|
||||
.from(engagements)
|
||||
.where(like(engagements.summary, `${prefix}%`))
|
||||
.limit(1);
|
||||
if (existing.length > 0) {
|
||||
return { engagements: 0, artifacts: 0, scores: 0, promoted: 0 };
|
||||
}
|
||||
|
||||
const playbook = await findTemplate(context, 'strategic-deployment-playbook');
|
||||
const scorecard = await findTemplate(context, 'trainability-qualification');
|
||||
const pocTemplate = await findTemplate(context, 'post-training-poc');
|
||||
const caseStudy = await findTemplate(context, 'case-study-frame');
|
||||
|
||||
let engagementCount = 0;
|
||||
let artifactCount = 0;
|
||||
let scoreCount = 0;
|
||||
let promotedCount = 0;
|
||||
|
||||
// ------------------------------------------------- mid-POC, still scoring
|
||||
const halcyonDealId = await findDeal(context, `${prefix}Managed post-training run`);
|
||||
const ines = await findSeller(context, 'Ines Fabre');
|
||||
|
||||
/*
|
||||
* A missing deal is reported, not skipped in silence.
|
||||
*
|
||||
* Both engagements hang off deals the demand book creates, and that book
|
||||
* skips an account it has already seen — so a database where the accounts
|
||||
* survive but the deals were removed leaves these lookups empty and this
|
||||
* whole section a no-op that reports zeroes and looks like a bug in the
|
||||
* loader. It was found exactly that way, on a shared development database.
|
||||
*/
|
||||
if (!halcyonDealId) {
|
||||
console.warn(
|
||||
` No "${prefix}Managed post-training run" deal — skipping the mid-POC engagement. ` +
|
||||
'Run `pnpm db:demo -- --clear` and reseed to rebuild the demand book.',
|
||||
);
|
||||
}
|
||||
|
||||
if (halcyonDealId) {
|
||||
const [engagement] = await db
|
||||
.insert(engagements)
|
||||
.values({
|
||||
demandDealId: halcyonDealId,
|
||||
playbookTemplateId: playbook?.id ?? null,
|
||||
ownerUserId: ines,
|
||||
status: 'open',
|
||||
summary: `${prefix}Post-training motion on the Halcyon summarisation workflow — POC week two.`,
|
||||
openedAt: at(-58),
|
||||
})
|
||||
.returning({ id: engagements.id });
|
||||
if (engagement) {
|
||||
engagementCount += 1;
|
||||
|
||||
await db.insert(engagementArtifacts).values({
|
||||
engagementId: engagement.id,
|
||||
templateId: scorecard?.id ?? null,
|
||||
kind: 'discovery',
|
||||
stage: 'qualification',
|
||||
title: `${prefix}Discovery notes — Halcyon summarisation`,
|
||||
body:
|
||||
'## What they are trying to do\n\n' +
|
||||
'Reduce reviewer time on internal research summaries. Today a senior analyst reads a ' +
|
||||
'40-page source and writes a one-page brief; the bar is "would the head of research ' +
|
||||
'send this out unedited".\n\n' +
|
||||
'## Unit of work\n\n' +
|
||||
'One source document in, one brief out. They have 4,100 historical pairs and the ' +
|
||||
'reviewer decision on each.\n\n' +
|
||||
'## Verifier\n\n' +
|
||||
'A model judge calibrated against 300 of those human decisions. Agreement is 0.81 ' +
|
||||
'Cohen’s kappa, measured on a held-out slice rather than the calibration set.\n\n' +
|
||||
'## The live risk\n\n' +
|
||||
'Trace export beyond the pilot needs a DPA amendment their counsel has not seen yet.',
|
||||
status: 'final',
|
||||
authoredByUserId: ines,
|
||||
fields: { source: 'Technical discovery call, plus the follow-up with the data owner.' },
|
||||
});
|
||||
|
||||
await db.insert(engagementArtifacts).values({
|
||||
engagementId: engagement.id,
|
||||
templateId: pocTemplate?.id ?? null,
|
||||
kind: 'poc',
|
||||
stage: 'poc',
|
||||
title: `${prefix}POC plan — summarisation reward model`,
|
||||
body:
|
||||
'## Success criterion, agreed in writing\n\n' +
|
||||
'Judge-scored acceptance rate on a held-out set of 400 briefs rises from the measured ' +
|
||||
'baseline of 61% to at least 78%, at no more than 1.4× the current inference cost ' +
|
||||
'per brief.\n\n' +
|
||||
'## What we run\n\n' +
|
||||
'Two weeks on 32× H100. Week one builds the environment and reproduces the ' +
|
||||
'baseline; week two is the training run and the evaluation.\n\n' +
|
||||
'## What we hand back\n\n' +
|
||||
'The environment, the eval harness, and the number — whichever way it comes out. ' +
|
||||
'A POC that reports a failure honestly is what makes the next number believable.\n\n' +
|
||||
'## Open\n\n' +
|
||||
'Trace rights beyond the pilot. Blocked on the DPA amendment.',
|
||||
status: 'review',
|
||||
authoredByUserId: ines,
|
||||
fields: { baselineAcceptancePct: 61, targetAcceptancePct: 78, gpuCount: 32, weeks: 2 },
|
||||
});
|
||||
artifactCount += 2;
|
||||
|
||||
for (const pass of SCORE_PASSES) {
|
||||
const dimensions = dimensionsFor(pass.scores);
|
||||
const basisPoints = motionScoreBasisPoints(dimensions);
|
||||
await db.insert(qualificationScores).values({
|
||||
engagementId: engagement.id,
|
||||
frameworkTemplateId: scorecard?.id ?? null,
|
||||
// The column is `Record<string, unknown>[]`; Drizzle's jsonb `$type`
|
||||
// will not take an interface with readonly properties, and widening
|
||||
// the column to suit the seed would lose the shape everywhere else.
|
||||
dimensions: dimensions as unknown as Record<string, unknown>[],
|
||||
basisPoints,
|
||||
band: motionBand(basisPoints).label,
|
||||
note: `${prefix}${pass.note}`,
|
||||
scoredByUserId: ines,
|
||||
scoredAt: at(-pass.daysAgo),
|
||||
});
|
||||
scoreCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------- closed won, and promoted afterwards
|
||||
const northwindDealId = await findDeal(context, `${prefix}A100 inference pilot`);
|
||||
const marcus = await findSeller(context, 'Marcus Oyelaran');
|
||||
|
||||
if (!northwindDealId) {
|
||||
console.warn(
|
||||
` No "${prefix}A100 inference pilot" deal — skipping the closed-won engagement, and ` +
|
||||
'with it the promotion that demonstrates the loop.',
|
||||
);
|
||||
}
|
||||
|
||||
if (northwindDealId) {
|
||||
const [engagement] = await db
|
||||
.insert(engagements)
|
||||
.values({
|
||||
demandDealId: northwindDealId,
|
||||
playbookTemplateId: playbook?.id ?? null,
|
||||
ownerUserId: marcus,
|
||||
status: 'won',
|
||||
summary: `${prefix}Northwind inference pilot — won, and written up.`,
|
||||
openedAt: at(-190),
|
||||
closedAt: at(-96),
|
||||
})
|
||||
.returning({ id: engagements.id });
|
||||
|
||||
if (engagement) {
|
||||
engagementCount += 1;
|
||||
|
||||
const [artifact] = await db
|
||||
.insert(engagementArtifacts)
|
||||
.values({
|
||||
engagementId: engagement.id,
|
||||
templateId: caseStudy?.id ?? null,
|
||||
kind: 'case_study',
|
||||
stage: 'expansion',
|
||||
title: `${prefix}Northwind Robotics — A100 inference pilot`,
|
||||
body:
|
||||
'## The problem\n\n' +
|
||||
'Northwind ran perception inference on reserved A100s they had bought for training, ' +
|
||||
'and were paying training prices for a serving workload with a 40:1 peak-to-trough ' +
|
||||
'shape.\n\n' +
|
||||
'## What we did\n\n' +
|
||||
'Moved the trough onto committed capacity and the peak onto burst, with the split ' +
|
||||
'set by their own 90-day request trace rather than by a headline number.\n\n' +
|
||||
'## The number\n\n' +
|
||||
'Cost per million inferences fell 34%. Measured against their own billing, over a ' +
|
||||
'full quarter, not against a list price.\n\n' +
|
||||
'## What made it work, and what to reuse\n\n' +
|
||||
'They had the request trace. Every engagement since has asked for it in the first ' +
|
||||
'call, which is the change this write-up put into the frame itself.',
|
||||
status: 'final',
|
||||
authoredByUserId: marcus,
|
||||
fields: {
|
||||
metric: 'Cost per million inferences',
|
||||
improvementPct: 34,
|
||||
window: 'One quarter, customer billing',
|
||||
approvedForExternalUse: false,
|
||||
},
|
||||
})
|
||||
.returning({ id: engagementArtifacts.id, title: engagementArtifacts.title });
|
||||
|
||||
artifactCount += 1;
|
||||
|
||||
/*
|
||||
* The promotion, written by hand rather than by calling the API service:
|
||||
* the seed has no HTTP surface and no principal. It follows the same
|
||||
* rules the service does — version = previous + 1, `supersedesId` to the
|
||||
* shipped v1, `originArtifactId` to the artifact, `visibility: 'shared'`,
|
||||
* `isSystem: false` — because a demo that produced a row the real path
|
||||
* could not have produced would teach the wrong shape of the table.
|
||||
*
|
||||
* `isSystem` is false and the title is prefixed, which is what lets
|
||||
* `clear()` find it. It is a version 2 of a shipped lineage, so removing
|
||||
* the demo book genuinely returns the library to what ships.
|
||||
*/
|
||||
if (artifact && caseStudy) {
|
||||
const [promoted] = await db
|
||||
.insert(motionTemplates)
|
||||
.values({
|
||||
kind: 'case_study',
|
||||
stage: 'expansion',
|
||||
slug: 'case-study-frame',
|
||||
version: caseStudy.version + 1,
|
||||
title: `${prefix}Case Study Frame — ask for the request trace first`,
|
||||
summary:
|
||||
'Version 2, promoted out of the Northwind pilot write-up. Adds the one question ' +
|
||||
'that made that engagement measurable: get the customer’s own usage trace ' +
|
||||
'before anyone quotes a saving.',
|
||||
body:
|
||||
'_Promoted from an engagement. The change from version 1 is the section below._\n\n' +
|
||||
'## Before you frame anything: get the trace\n\n' +
|
||||
'A case study is only as good as the baseline it is measured against, and the ' +
|
||||
'only baseline a customer cannot argue with afterwards is their own telemetry. ' +
|
||||
'Ask for it in the first call, not at write-up time.\n\n' +
|
||||
'## Everything else\n\n' +
|
||||
'As version 1: the problem, what we did, the number, what to reuse.',
|
||||
fields: {
|
||||
promotedFrom: artifact.title,
|
||||
addedSection: 'Before you frame anything: get the trace',
|
||||
},
|
||||
visibility: 'shared',
|
||||
ownerUserId: marcus,
|
||||
supersedesId: caseStudy.id,
|
||||
originArtifactId: artifact.id,
|
||||
isSystem: false,
|
||||
})
|
||||
.onConflictDoNothing({ target: [motionTemplates.slug, motionTemplates.version] })
|
||||
.returning({ id: motionTemplates.id });
|
||||
|
||||
if (promoted) {
|
||||
await db
|
||||
.update(engagementArtifacts)
|
||||
.set({ promotedTemplateId: promoted.id })
|
||||
.where(eq(engagementArtifacts.id, artifact.id));
|
||||
promotedCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* `usage_count` is what the API reads to refuse a `PATCH` on a used
|
||||
* template, so the demo must move it or a library where three templates have
|
||||
* visibly been instantiated still reads as never used — and the rule the
|
||||
* whole feature turns on would be undemonstrable. Incremented in one sweep at
|
||||
* the end rather than per insert, so the count is derived from the artefacts
|
||||
* that actually landed and `clear()` can undo it by counting the same rows.
|
||||
*/
|
||||
const instantiated = await db
|
||||
.select({ templateId: engagementArtifacts.templateId })
|
||||
.from(engagementArtifacts)
|
||||
.where(like(engagementArtifacts.title, `${prefix}%`));
|
||||
const usageByTemplate = new Map<string, number>();
|
||||
for (const row of instantiated) {
|
||||
if (row.templateId) usageByTemplate.set(row.templateId, (usageByTemplate.get(row.templateId) ?? 0) + 1);
|
||||
}
|
||||
for (const [templateId, count] of usageByTemplate) {
|
||||
await db
|
||||
.update(motionTemplates)
|
||||
.set({ usageCount: sql`${motionTemplates.usageCount} + ${count}` })
|
||||
.where(eq(motionTemplates.id, templateId));
|
||||
}
|
||||
|
||||
return {
|
||||
engagements: engagementCount,
|
||||
artifacts: artifactCount,
|
||||
scores: scoreCount,
|
||||
promoted: promotedCount,
|
||||
};
|
||||
}
|
||||
@@ -14,6 +14,9 @@
|
||||
* • **A worked example** of the thing PIG exists for: one capacity
|
||||
* commitment, two allocations against it, and therefore a real margin
|
||||
* number and a real idle-capacity alert on the dashboard.
|
||||
* • **The Motion starter library.** Authored content shipped with the
|
||||
* product rather than invented data, which is why it lives here and not in
|
||||
* `demo/`. See `motion/index.ts`.
|
||||
*
|
||||
* The example is clearly labelled, and the company buying is invented. Real
|
||||
* named companies appear here only with a source; commercial terms attached to
|
||||
@@ -31,6 +34,7 @@ import {
|
||||
teamMemberships,
|
||||
users,
|
||||
} from '../schema/index';
|
||||
import { seedMotionLibrary } from './motion/index';
|
||||
import { PRIME_INTELLECT_PEOPLE, PUBLIC_CUSTOMER_REFERENCES, UNRESOLVED_NAMES } from './people';
|
||||
|
||||
const db = createDatabase();
|
||||
@@ -343,6 +347,16 @@ async function seed() {
|
||||
console.log(' Worked example already present — skipped.');
|
||||
}
|
||||
|
||||
// ------------------------------------------------ the Motion starter library
|
||||
const motion = await seedMotionLibrary(db);
|
||||
console.log(
|
||||
` ${motion.total} Motion starter template(s) (${motion.added} new) — authored content, ` +
|
||||
'shared and system-owned, version 1 of their lineages.',
|
||||
);
|
||||
for (const bad of motion.rejected) {
|
||||
console.error(` SKIPPED a starter template with a value outside the ontology: ${bad}`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- the dev user
|
||||
//
|
||||
// Only when the table is empty. With authentication disabled in development
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* The starter library — twelve authored templates, shipped with the product.
|
||||
*
|
||||
* These are **not** demo data and carry no `DEMO — ` prefix. They are written
|
||||
* content in the same class as `docs/learn-scripts.md`: a customer who
|
||||
* self-hosts PIG gets them, uses them against real deals, and promotes their
|
||||
* own versions back over the top. That is why they seed `isSystem: true`,
|
||||
* `visibility: 'shared'`, `version: 1` and `ownerUserId: null` — a shared
|
||||
* system row belongs to the deployment rather than to whoever ran the seed,
|
||||
* and version 1 is the base of a lineage that a customer's promotions extend.
|
||||
*
|
||||
* The prose lives in JSON beside this file rather than in a TypeScript literal
|
||||
* because the bodies are long-form markdown — the shortest is 7kB and the
|
||||
* longest 24kB — and a 200kB module of backtick strings is unreviewable and
|
||||
* unmergeable by two people at once. They are imported statically with an
|
||||
* import attribute rather than read with `readdir`, which was tried first: a
|
||||
* static import makes a missing or renamed file a typecheck failure instead of
|
||||
* a seed that quietly ships eleven templates. Both `tsx` and `tsc` were run
|
||||
* against this to confirm the attribute syntax is understood by each, since
|
||||
* the server runs the TypeScript directly.
|
||||
*
|
||||
* Idempotency is `onConflictDoNothing({ target: [slug, version] })`, which
|
||||
* works **only** because of `motion_templates_slug_version_key`. AGENTS.md §5
|
||||
* is blunt about what that clause does without a constraint to fire on — it
|
||||
* silently duplicated seed data here twice — so `assertNoDuplicates` below
|
||||
* checks the outcome rather than trusting the schema, and CI counts this table
|
||||
* in its idempotency gate alongside `contacts`.
|
||||
*/
|
||||
import { DEMAND_STAGES, isMotionKind, type DemandStage, type MotionKind } from '@pig/core';
|
||||
import { and, eq, inArray } from 'drizzle-orm';
|
||||
import type { Database } from '../../client';
|
||||
import { motionTemplates } from '../../schema/index';
|
||||
|
||||
import caseStudyFrame from './case-study-frame.json' with { type: 'json' };
|
||||
import dataAndSecurityBrief from './data-and-security-brief.json' with { type: 'json' };
|
||||
import postTrainingPoc from './post-training-poc.json' with { type: 'json' };
|
||||
import pricingAndPackaging from './pricing-and-packaging.json' with { type: 'json' };
|
||||
import procurementAndBudgetPath from './procurement-and-budget-path.json' with { type: 'json' };
|
||||
import productionReadiness from './production-readiness.json' with { type: 'json' };
|
||||
import proposalBlocks from './proposal-blocks.json' with { type: 'json' };
|
||||
import referenceArchitectures from './reference-architectures.json' with { type: 'json' };
|
||||
import strategicDeploymentPlaybook from './strategic-deployment-playbook.json' with { type: 'json' };
|
||||
import technicalDiscovery from './technical-discovery.json' with { type: 'json' };
|
||||
import technicalNarratives from './technical-narratives.json' with { type: 'json' };
|
||||
import trainabilityQualification from './trainability-qualification.json' with { type: 'json' };
|
||||
|
||||
/**
|
||||
* The authored shape. `kind` and `stage` are `string` here and narrowed at the
|
||||
* boundary below, because `resolveJsonModule` infers `string` for a JSON string
|
||||
* and there is no honest way to tell TypeScript otherwise without a cast that
|
||||
* would also swallow a genuine typo in the file.
|
||||
*/
|
||||
interface StarterTemplate {
|
||||
readonly kind: string;
|
||||
readonly slug: string;
|
||||
readonly title: string;
|
||||
readonly summary: string;
|
||||
readonly stage: string;
|
||||
readonly body: string;
|
||||
readonly fields: Record<string, unknown>;
|
||||
/** Guidance for whoever runs it. Folded into `fields` — see `toRow`. */
|
||||
readonly notes: string;
|
||||
}
|
||||
|
||||
const STARTER_LIBRARY: readonly StarterTemplate[] = [
|
||||
technicalDiscovery,
|
||||
dataAndSecurityBrief,
|
||||
trainabilityQualification,
|
||||
referenceArchitectures,
|
||||
technicalNarratives,
|
||||
postTrainingPoc,
|
||||
proposalBlocks,
|
||||
pricingAndPackaging,
|
||||
procurementAndBudgetPath,
|
||||
productionReadiness,
|
||||
caseStudyFrame,
|
||||
strategicDeploymentPlaybook,
|
||||
];
|
||||
|
||||
const isDemandStage = (value: string): value is DemandStage =>
|
||||
(DEMAND_STAGES as readonly string[]).includes(value);
|
||||
|
||||
/**
|
||||
* `notes` has no column, deliberately.
|
||||
*
|
||||
* It is guidance for the person running the template — when to reach for it,
|
||||
* what it is not for — rather than a field the artifact renders, so a column
|
||||
* would put it on every engagement artifact's editor for no reason. Keeping it
|
||||
* inside `fields` means promotion carries it forward with the rest of the
|
||||
* structured payload without any special handling in the promote path.
|
||||
*/
|
||||
function toRow(
|
||||
template: StarterTemplate,
|
||||
kind: MotionKind,
|
||||
stage: DemandStage,
|
||||
): typeof motionTemplates.$inferInsert {
|
||||
return {
|
||||
kind,
|
||||
stage,
|
||||
slug: template.slug,
|
||||
version: 1,
|
||||
title: template.title,
|
||||
summary: template.summary,
|
||||
body: template.body,
|
||||
fields: { ...template.fields, notes: template.notes },
|
||||
visibility: 'shared',
|
||||
ownerUserId: null,
|
||||
isSystem: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirm the outcome, not the constraint.
|
||||
*
|
||||
* Reading `pg_constraint` would prove the unique index exists; counting the
|
||||
* rows proves the clause did its job, which is the thing that has actually
|
||||
* gone wrong here before. It throws rather than warns: a duplicated starter
|
||||
* library is a corrupt library, and every subsequent run would double it again.
|
||||
*
|
||||
* Scoped to version 1, and found by running it: the demo book promotes an
|
||||
* artifact into `case-study-frame` v2, so counting every row for these slugs
|
||||
* reported the loop working as a duplicated seed. Version 1 is the only row
|
||||
* this loader writes and therefore the only one it may assert about.
|
||||
*/
|
||||
async function assertNoDuplicates(db: Database, slugs: readonly string[]): Promise<number> {
|
||||
const rows = await db
|
||||
.select({ slug: motionTemplates.slug })
|
||||
.from(motionTemplates)
|
||||
.where(and(inArray(motionTemplates.slug, [...slugs]), eq(motionTemplates.version, 1)));
|
||||
|
||||
if (rows.length > slugs.length) {
|
||||
const counts = new Map<string, number>();
|
||||
for (const row of rows) counts.set(row.slug, (counts.get(row.slug) ?? 0) + 1);
|
||||
const duplicated = [...counts.entries()].filter(([, count]) => count > 1).map(([slug]) => slug);
|
||||
throw new Error(
|
||||
`The starter library has duplicate rows for: ${duplicated.join(', ')}. ` +
|
||||
'The unique constraint motion_templates_slug_version_key is missing or the ' +
|
||||
'conflict target no longer matches it — see AGENTS.md §5.',
|
||||
);
|
||||
}
|
||||
|
||||
return rows.length;
|
||||
}
|
||||
|
||||
export async function seedMotionLibrary(
|
||||
db: Database,
|
||||
): Promise<{ total: number; added: number; rejected: readonly string[] }> {
|
||||
const rejected: string[] = [];
|
||||
let added = 0;
|
||||
|
||||
for (const template of STARTER_LIBRARY) {
|
||||
/*
|
||||
* A bad `kind` or `stage` is skipped, not thrown on. These files are
|
||||
* authored by hand and the CHECK constraints would reject the row anyway —
|
||||
* but as an aborted transaction partway through the base seed, taking the
|
||||
* eleven good templates and everything after them down with it. Reporting
|
||||
* the one bad file by name is more useful and leaves the deployment usable.
|
||||
*/
|
||||
if (!isMotionKind(template.kind) || !isDemandStage(template.stage)) {
|
||||
rejected.push(`${template.slug} (kind=${template.kind}, stage=${template.stage})`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const [created] = await db
|
||||
.insert(motionTemplates)
|
||||
.values(toRow(template, template.kind, template.stage))
|
||||
.onConflictDoNothing({ target: [motionTemplates.slug, motionTemplates.version] })
|
||||
.returning({ id: motionTemplates.id });
|
||||
if (created) added += 1;
|
||||
}
|
||||
|
||||
const total = await assertNoDuplicates(
|
||||
db,
|
||||
STARTER_LIBRARY.map((template) => template.slug),
|
||||
);
|
||||
|
||||
return { total, added, rejected };
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user