Files
pig/packages/db/migrations/0014_motion.sql
T
karti 516685526c 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>
2026-08-17 18:27:03 -07:00

107 lines
8.1 KiB
SQL

-- 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);