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:
@@ -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 };
|
||||
}
|
||||
Reference in New Issue
Block a user