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:
2026-08-17 18:27:03 -07:00
parent 99d165b5e5
commit 516685526c
61 changed files with 13013 additions and 29 deletions
+178
View File
@@ -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 };
}