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,106 @@
|
||||
-- Motion: the library, engagements, artifacts and the append-only score log.
|
||||
--
|
||||
-- Hand-written, and it has to be. `motion_templates.origin_artifact_id`
|
||||
-- references `engagement_artifacts`, and `engagement_artifacts.template_id`
|
||||
-- references `motion_templates` — the promotion loop is a foreign key cycle,
|
||||
-- and there is no ordering of two CREATE TABLE statements that satisfies both.
|
||||
-- Drizzle emits every constraint with the table it belongs to and will not
|
||||
-- order this for you, so the second half of the cycle is added below as its own
|
||||
-- ALTER TABLE once both tables exist. Generated output for this schema fails on
|
||||
-- `relation "engagement_artifacts" does not exist`.
|
||||
CREATE TABLE "motion_templates" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"kind" text NOT NULL,
|
||||
"slug" text NOT NULL,
|
||||
"version" integer DEFAULT 1 NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"summary" text NOT NULL,
|
||||
"body" text NOT NULL,
|
||||
"fields" jsonb,
|
||||
"stage" text NOT NULL,
|
||||
"visibility" text DEFAULT 'private' NOT NULL,
|
||||
"owner_user_id" uuid,
|
||||
"supersedes_id" uuid,
|
||||
"origin_artifact_id" uuid,
|
||||
"is_system" boolean DEFAULT false NOT NULL,
|
||||
"usage_count" integer DEFAULT 0 NOT NULL,
|
||||
"archived_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "motion_templates_slug_version_key" UNIQUE("slug","version"),
|
||||
CONSTRAINT "motion_templates_kind_check" CHECK ("motion_templates"."kind" IN ('discovery', 'qualification', 'poc', 'proposal', 'pricing', 'architecture', 'case_study', 'narrative', 'playbook')),
|
||||
CONSTRAINT "motion_templates_stage_check" CHECK ("motion_templates"."stage" IN ('qualification', 'legal', 'scoping', 'proposal', 'procurement', 'poc', 'deployment', 'expansion', 'closed_won', 'closed_lost')),
|
||||
CONSTRAINT "motion_templates_visibility_check" CHECK ("motion_templates"."visibility" IN ('private', 'shared')),
|
||||
CONSTRAINT "motion_templates_version_positive_check" CHECK ("motion_templates"."version" > 0),
|
||||
CONSTRAINT "motion_templates_private_has_owner_check" CHECK ("motion_templates"."visibility" <> 'private' OR "motion_templates"."owner_user_id" IS NOT NULL)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "engagements" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"demand_deal_id" uuid NOT NULL,
|
||||
"playbook_template_id" uuid,
|
||||
"owner_user_id" uuid,
|
||||
"status" text DEFAULT 'open' NOT NULL,
|
||||
"summary" text,
|
||||
"opened_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"closed_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "engagements_demand_deal_key" UNIQUE("demand_deal_id"),
|
||||
CONSTRAINT "engagements_status_check" CHECK ("engagements"."status" IN ('open', 'won', 'lost', 'paused'))
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "engagement_artifacts" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"engagement_id" uuid NOT NULL,
|
||||
"template_id" uuid,
|
||||
"kind" text NOT NULL,
|
||||
"stage" text NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"body" text NOT NULL,
|
||||
"fields" jsonb,
|
||||
"status" text DEFAULT 'draft' NOT NULL,
|
||||
"authored_by_user_id" uuid,
|
||||
"promoted_template_id" uuid,
|
||||
"archived_at" timestamp with time zone,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "engagement_artifacts_kind_check" CHECK ("engagement_artifacts"."kind" IN ('discovery', 'qualification', 'poc', 'proposal', 'pricing', 'architecture', 'case_study', 'narrative', 'playbook')),
|
||||
CONSTRAINT "engagement_artifacts_stage_check" CHECK ("engagement_artifacts"."stage" IN ('qualification', 'legal', 'scoping', 'proposal', 'procurement', 'poc', 'deployment', 'expansion', 'closed_won', 'closed_lost')),
|
||||
CONSTRAINT "engagement_artifacts_status_check" CHECK ("engagement_artifacts"."status" IN ('draft', 'review', 'final'))
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "qualification_scores" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"engagement_id" uuid NOT NULL,
|
||||
"framework_template_id" uuid,
|
||||
"dimensions" jsonb NOT NULL,
|
||||
"basis_points" integer NOT NULL,
|
||||
"band" text NOT NULL,
|
||||
"note" text,
|
||||
"scored_by_user_id" uuid,
|
||||
"scored_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT "qualification_scores_basis_points_check" CHECK ("qualification_scores"."basis_points" >= 0 AND "qualification_scores"."basis_points" <= 10000)
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "motion_templates" ADD CONSTRAINT "motion_templates_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "motion_templates" ADD CONSTRAINT "motion_templates_supersedes_id_motion_templates_id_fk" FOREIGN KEY ("supersedes_id") REFERENCES "public"."motion_templates"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "engagements" ADD CONSTRAINT "engagements_demand_deal_id_demand_deals_id_fk" FOREIGN KEY ("demand_deal_id") REFERENCES "public"."demand_deals"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "engagements" ADD CONSTRAINT "engagements_playbook_template_id_motion_templates_id_fk" FOREIGN KEY ("playbook_template_id") REFERENCES "public"."motion_templates"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "engagements" ADD CONSTRAINT "engagements_owner_user_id_users_id_fk" FOREIGN KEY ("owner_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "engagement_artifacts" ADD CONSTRAINT "engagement_artifacts_engagement_id_engagements_id_fk" FOREIGN KEY ("engagement_id") REFERENCES "public"."engagements"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "engagement_artifacts" ADD CONSTRAINT "engagement_artifacts_template_id_motion_templates_id_fk" FOREIGN KEY ("template_id") REFERENCES "public"."motion_templates"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "engagement_artifacts" ADD CONSTRAINT "engagement_artifacts_authored_by_user_id_users_id_fk" FOREIGN KEY ("authored_by_user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "engagement_artifacts" ADD CONSTRAINT "engagement_artifacts_promoted_template_fk" FOREIGN KEY ("promoted_template_id") REFERENCES "public"."motion_templates"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
-- The other half of the loop, and the reason this file is hand-written.
|
||||
ALTER TABLE "motion_templates" ADD CONSTRAINT "motion_templates_origin_artifact_fk" FOREIGN KEY ("origin_artifact_id") REFERENCES "public"."engagement_artifacts"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "qualification_scores" ADD CONSTRAINT "qualification_scores_engagement_id_engagements_id_fk" FOREIGN KEY ("engagement_id") REFERENCES "public"."engagements"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "qualification_scores" ADD CONSTRAINT "qualification_scores_framework_template_fk" FOREIGN KEY ("framework_template_id") REFERENCES "public"."motion_templates"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "motion_templates_kind_stage_idx" ON "motion_templates" USING btree ("kind","stage");--> statement-breakpoint
|
||||
CREATE INDEX "motion_templates_visibility_kind_idx" ON "motion_templates" USING btree ("visibility","kind");--> statement-breakpoint
|
||||
CREATE INDEX "motion_templates_owner_idx" ON "motion_templates" USING btree ("owner_user_id");--> statement-breakpoint
|
||||
CREATE INDEX "engagements_status_idx" ON "engagements" USING btree ("status");--> statement-breakpoint
|
||||
CREATE INDEX "engagement_artifacts_engagement_stage_idx" ON "engagement_artifacts" USING btree ("engagement_id","stage");--> statement-breakpoint
|
||||
CREATE INDEX "qualification_scores_engagement_idx" ON "qualification_scores" USING btree ("engagement_id","scored_at" DESC);
|
||||
@@ -99,6 +99,13 @@
|
||||
"when": 1786700000000,
|
||||
"tag": "0013_learn_self_hosted_provider",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "7",
|
||||
"when": 1786800000000,
|
||||
"tag": "0014_motion",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* contracts MSA, DPA, SLA, order forms, obligations
|
||||
* compliance export control as a predicate on the match
|
||||
* calendar the one dated row type nothing else owns
|
||||
* motion the reusable practice bound to the demand stages
|
||||
* agent the leased task queue and evidence-bearing facts
|
||||
* fields user-defined fields
|
||||
*/
|
||||
@@ -26,6 +27,7 @@ export * from './contracts';
|
||||
export * from './compliance';
|
||||
export * from './calendar';
|
||||
export * from './learn';
|
||||
export * from './motion';
|
||||
export * from './agent';
|
||||
export * from './fields';
|
||||
export * from './integrations';
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
/**
|
||||
* Motion — the library, the engagements, and the loop between them.
|
||||
*
|
||||
* Four tables, and three of the decisions in them are load-bearing.
|
||||
*
|
||||
* **A used template is never edited in place.** `usage_count` is incremented
|
||||
* when a template is instantiated into an engagement, and the API refuses a
|
||||
* `PATCH` once it is above zero — a new version row is written instead,
|
||||
* carrying `supersedes_id` back to its predecessor and `origin_artifact_id`
|
||||
* forward to the artifact that proved it. A live engagement whose template
|
||||
* changed underneath it has lost its provenance, and provenance is the only
|
||||
* thing that makes "this came from playbook v3" mean anything a quarter later.
|
||||
* That is also why `(slug, version)` is unique: the seed writes
|
||||
* `onConflictDoNothing({ target: [slug, version] })`, which is a silent no-op
|
||||
* without a constraint to conflict on, and that has already duplicated seed
|
||||
* data twice in this codebase.
|
||||
*
|
||||
* **`visibility = 'private'` is a real access rule, and it is this codebase's
|
||||
* first row-level filter.** `permissions.ts` says plainly that every read
|
||||
* endpoint returns the whole book, because no row-level team filter exists
|
||||
* anywhere in the query layer. Motion is a deliberate exception, stated here
|
||||
* so it cannot be discovered by surprise: a draft proposal for a live deal is
|
||||
* not the same object as a contract, and a library nobody can draft in
|
||||
* privately becomes a library nobody drafts in. `shared` is book-wide on
|
||||
* `book:read` exactly like everything else; `private` is readable and writable
|
||||
* by `owner_user_id` and by a platform admin, and by nobody else.
|
||||
*
|
||||
* The CHECK below refuses a private row with no owner, because such a row is
|
||||
* readable by nobody and writable by nobody — a leak the day someone "fixes"
|
||||
* the query that appears to be dropping rows. It also, measured by running the
|
||||
* delete rather than by reading the DDL, makes `ON DELETE SET NULL` on
|
||||
* `owner_user_id` unreachable for a private row: Postgres evaluates the CHECK
|
||||
* on the UPDATE the referential action performs, so deleting a user who owns
|
||||
* one raises `motion_templates_private_has_owner_check` and the delete fails.
|
||||
* Private authorship therefore blocks a user delete today, and whoever adds a
|
||||
* member-removal path has to archive or reassign those rows first. The read
|
||||
* query still treats a private row with a null owner as invisible to everyone
|
||||
* but a platform admin, so that relaxing the CHECK cannot quietly publish
|
||||
* them — do not write that filter as `owner_user_id = $me OR owner_user_id IS
|
||||
* NULL`.
|
||||
*
|
||||
* **`qualification_scores` is append-only.** Never updated, never deleted. The
|
||||
* movement of a score across an engagement is the evidence that qualification
|
||||
* happened at all; a mutable current score is a number somebody can make true
|
||||
* afterwards. Everything else archives rather than deletes, as elsewhere in
|
||||
* PIG.
|
||||
*
|
||||
* The FK cycle between `motion_templates.origin_artifact_id` and
|
||||
* `engagement_artifacts.template_id` is intentional — it is the loop — and it
|
||||
* is why migration `0014_motion.sql` is hand-written, adding one of the two
|
||||
* constraints in a separate `ALTER TABLE` after both tables exist.
|
||||
*/
|
||||
import { sql } from 'drizzle-orm';
|
||||
import {
|
||||
boolean,
|
||||
check,
|
||||
index,
|
||||
integer,
|
||||
jsonb,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
unique,
|
||||
uuid,
|
||||
type AnyPgColumn,
|
||||
} from 'drizzle-orm/pg-core';
|
||||
import {
|
||||
ARTIFACT_STATUSES,
|
||||
DEMAND_STAGES,
|
||||
ENGAGEMENT_STATUSES,
|
||||
MOTION_BASIS_POINTS_MAX,
|
||||
MOTION_KINDS,
|
||||
MOTION_VISIBILITIES,
|
||||
} from '@pig/core';
|
||||
import { demandDeals } from './demand';
|
||||
import { users } from './identity';
|
||||
|
||||
/**
|
||||
* Render a value set as a SQL `IN` list from the ontology constant.
|
||||
*
|
||||
* Copied from `learn.ts` rather than imported: it is a private detail of how a
|
||||
* schema file renders its constraints, and exporting it would make two files
|
||||
* that must be able to diverge share one. The values are compile-time literal
|
||||
* constants from `@pig/core`, never input.
|
||||
*/
|
||||
const inList = (values: readonly string[]) =>
|
||||
sql.raw(values.map((value) => `'${value}'`).join(', '));
|
||||
|
||||
export const motionTemplates = pgTable(
|
||||
'motion_templates',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
kind: text('kind', { enum: MOTION_KINDS }).notNull(),
|
||||
|
||||
/** Stable across versions: the slug, not the id, is the identity of a lineage. */
|
||||
slug: text('slug').notNull(),
|
||||
version: integer('version').notNull().default(1),
|
||||
|
||||
title: text('title').notNull(),
|
||||
summary: text('summary').notNull(),
|
||||
/** Markdown. Rendered by the web app; never framed, never executed. */
|
||||
body: text('body').notNull(),
|
||||
/** The structured half — scoring dimensions, pricing inputs, checklist items. */
|
||||
fields: jsonb('fields').$type<Record<string, unknown>>(),
|
||||
|
||||
stage: text('stage', { enum: DEMAND_STAGES }).notNull(),
|
||||
|
||||
visibility: text('visibility', { enum: MOTION_VISIBILITIES }).notNull().default('private'),
|
||||
ownerUserId: uuid('owner_user_id').references(() => users.id, { onDelete: 'set null' }),
|
||||
|
||||
/** The previous version in this lineage. Null on version 1. */
|
||||
supersedesId: uuid('supersedes_id').references((): AnyPgColumn => motionTemplates.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
/**
|
||||
* The engagement artifact this version was promoted from. Half of the
|
||||
* cycle the migration has to break — see the file header.
|
||||
*/
|
||||
originArtifactId: uuid('origin_artifact_id').references(
|
||||
(): AnyPgColumn => engagementArtifacts.id,
|
||||
{ onDelete: 'set null' },
|
||||
),
|
||||
|
||||
/** The starter library shipped with the product, not somebody's draft. */
|
||||
isSystem: boolean('is_system').notNull().default(false),
|
||||
/** Incremented on instantiate. Above zero, this row stops being editable. */
|
||||
usageCount: integer('usage_count').notNull().default(0),
|
||||
|
||||
archivedAt: timestamp('archived_at', { withTimezone: true }),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
check('motion_templates_kind_check', sql`${t.kind} IN (${inList(MOTION_KINDS)})`),
|
||||
check('motion_templates_stage_check', sql`${t.stage} IN (${inList(DEMAND_STAGES)})`),
|
||||
check(
|
||||
'motion_templates_visibility_check',
|
||||
sql`${t.visibility} IN (${inList(MOTION_VISIBILITIES)})`,
|
||||
),
|
||||
check('motion_templates_version_positive_check', sql`${t.version} > 0`),
|
||||
/**
|
||||
* Written as an implication so it reads as the rule it encodes: private
|
||||
* implies owned. An unowned private row is invisible to every query that
|
||||
* is written correctly, which is precisely why it must not exist.
|
||||
*/
|
||||
check(
|
||||
'motion_templates_private_has_owner_check',
|
||||
sql`${t.visibility} <> 'private' OR ${t.ownerUserId} IS NOT NULL`,
|
||||
),
|
||||
|
||||
/** Load-bearing for the seed's `onConflictDoNothing` — see the file header. */
|
||||
unique('motion_templates_slug_version_key').on(t.slug, t.version),
|
||||
|
||||
/** The library browse: a kind, at a stage. */
|
||||
index('motion_templates_kind_stage_idx').on(t.kind, t.stage),
|
||||
/** The shared-library read, which filters on visibility before anything else. */
|
||||
index('motion_templates_visibility_kind_idx').on(t.visibility, t.kind),
|
||||
/** "My drafts", the other half of the private/shared split. */
|
||||
index('motion_templates_owner_idx').on(t.ownerUserId),
|
||||
],
|
||||
);
|
||||
|
||||
export const engagements = pgTable(
|
||||
'engagements',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
/**
|
||||
* The spine. An engagement has no independent existence — it is the motion
|
||||
* being run against a deal that already exists, so it dies with the deal.
|
||||
*/
|
||||
demandDealId: uuid('demand_deal_id')
|
||||
.notNull()
|
||||
.references(() => demandDeals.id, { onDelete: 'cascade' }),
|
||||
|
||||
playbookTemplateId: uuid('playbook_template_id').references(() => motionTemplates.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
ownerUserId: uuid('owner_user_id').references(() => users.id, { onDelete: 'set null' }),
|
||||
|
||||
status: text('status', { enum: ENGAGEMENT_STATUSES }).notNull().default('open'),
|
||||
summary: text('summary'),
|
||||
|
||||
openedAt: timestamp('opened_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
closedAt: timestamp('closed_at', { withTimezone: true }),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
check('engagements_status_check', sql`${t.status} IN (${inList(ENGAGEMENT_STATUSES)})`),
|
||||
/**
|
||||
* One engagement per deal. Two engagements on one deal would give the same
|
||||
* opportunity two qualification histories and two answers to "what stage
|
||||
* is this at"; if that ever has to relax it relaxes deliberately.
|
||||
*/
|
||||
unique('engagements_demand_deal_key').on(t.demandDealId),
|
||||
index('engagements_status_idx').on(t.status),
|
||||
],
|
||||
);
|
||||
|
||||
export const engagementArtifacts = pgTable(
|
||||
'engagement_artifacts',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
engagementId: uuid('engagement_id')
|
||||
.notNull()
|
||||
.references(() => engagements.id, { onDelete: 'cascade' }),
|
||||
|
||||
/** What it was instantiated from. Null for an artifact written from scratch. */
|
||||
templateId: uuid('template_id').references(() => motionTemplates.id, { onDelete: 'set null' }),
|
||||
|
||||
kind: text('kind', { enum: MOTION_KINDS }).notNull(),
|
||||
stage: text('stage', { enum: DEMAND_STAGES }).notNull(),
|
||||
|
||||
title: text('title').notNull(),
|
||||
body: text('body').notNull(),
|
||||
fields: jsonb('fields').$type<Record<string, unknown>>(),
|
||||
|
||||
status: text('status', { enum: ARTIFACT_STATUSES }).notNull().default('draft'),
|
||||
authoredByUserId: uuid('authored_by_user_id').references(() => users.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
|
||||
/** Set once, by promotion. Its presence is what refuses a second promotion. */
|
||||
promotedTemplateId: uuid('promoted_template_id').references(() => motionTemplates.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
|
||||
archivedAt: timestamp('archived_at', { withTimezone: true }),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
check('engagement_artifacts_kind_check', sql`${t.kind} IN (${inList(MOTION_KINDS)})`),
|
||||
check('engagement_artifacts_stage_check', sql`${t.stage} IN (${inList(DEMAND_STAGES)})`),
|
||||
check('engagement_artifacts_status_check', sql`${t.status} IN (${inList(ARTIFACT_STATUSES)})`),
|
||||
/** The workspace reads one engagement's artifacts grouped by stage. */
|
||||
index('engagement_artifacts_engagement_stage_idx').on(t.engagementId, t.stage),
|
||||
],
|
||||
);
|
||||
|
||||
export const qualificationScores = pgTable(
|
||||
'qualification_scores',
|
||||
{
|
||||
id: uuid('id').primaryKey().defaultRandom(),
|
||||
|
||||
engagementId: uuid('engagement_id')
|
||||
.notNull()
|
||||
.references(() => engagements.id, { onDelete: 'cascade' }),
|
||||
frameworkTemplateId: uuid('framework_template_id').references(() => motionTemplates.id, {
|
||||
onDelete: 'set null',
|
||||
}),
|
||||
|
||||
/** `MotionDimensionScore[]` exactly as scored, so an old row survives a reweighted framework. */
|
||||
dimensions: jsonb('dimensions').$type<Record<string, unknown>[]>().notNull(),
|
||||
|
||||
/**
|
||||
* Basis points of the maximum, integer, computed by
|
||||
* `motionScoreBasisPoints`. Stored rather than derived because the
|
||||
* framework's weights can change and this number must not.
|
||||
*/
|
||||
basisPoints: integer('basis_points').notNull(),
|
||||
band: text('band').notNull(),
|
||||
note: text('note'),
|
||||
|
||||
scoredByUserId: uuid('scored_by_user_id').references(() => users.id, { onDelete: 'set null' }),
|
||||
scoredAt: timestamp('scored_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(t) => [
|
||||
check(
|
||||
'qualification_scores_basis_points_check',
|
||||
sql`${t.basisPoints} >= 0 AND ${t.basisPoints} <= ${sql.raw(String(MOTION_BASIS_POINTS_MAX))}`,
|
||||
),
|
||||
/** The history panel: one engagement, newest first. */
|
||||
index('qualification_scores_engagement_idx').on(t.engagementId, t.scoredAt.desc()),
|
||||
],
|
||||
);
|
||||
|
||||
export type MotionTemplate = typeof motionTemplates.$inferSelect;
|
||||
export type NewMotionTemplate = typeof motionTemplates.$inferInsert;
|
||||
export type Engagement = typeof engagements.$inferSelect;
|
||||
export type NewEngagement = typeof engagements.$inferInsert;
|
||||
export type EngagementArtifact = typeof engagementArtifacts.$inferSelect;
|
||||
export type NewEngagementArtifact = typeof engagementArtifacts.$inferInsert;
|
||||
export type QualificationScore = typeof qualificationScores.$inferSelect;
|
||||
export type NewQualificationScore = typeof qualificationScores.$inferInsert;
|
||||
@@ -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