15c72ade1c
Each was raised by a reviewer and then survived an independent attempt to refute it. The four that mattered most: - A third of the starter library was invisible. Three templates authored `fields` shapes no renderer read — decisions, blockingSet, checks, steps and the rest — so about forty records rendered as no DOM at all, in the library and again on the engagement that instantiated them. Nothing failed: a renderer returns null for a key set it does not recognise, and a header-plus-body page looks like a template written that way. FieldsView now reads every key the seeds carry. - "Add a framework" opened a picker that could never match, because the dialog was seeded with both the forced kind and the deal's stage, and qualification serves only the qualification stage. The stage is now dropped when MOTION_KIND_STAGES says the pair is incoherent. - Piggy reported the promotion count as an exact figure capped at 8, against a tile showing the true count beside it. It is now counted in SQL, and all three motion tools carry a ResultScope whose denominator is shared lineages — never rows, never private drafts. - No Motion test went through createApp, so the whole feature could be unmounted with a green suite. That is the AGENTS.md §5 trap that already cost this project read-guards.ts and learn.ts. Also: both sides of the instantiate/edit race now lock, so a template cannot be rewritten under an artefact that has copied it; concurrent engagement opens queue on the deal row and get the 409 the handler already promised rather than a 500; latestScore uses DISTINCT ON instead of losing engagements past a 200-row cap; the migration adds the scored_by_user_id foreign key the schema declares; and the demo clear refunds usage_count for engagements it reaches by cascade, which otherwise left starter templates permanently un-editable. Verified on a fresh database: 16 migrations apply and re-apply as a no-op, both seeds idempotent, usage_count back to zero after --clear. 564 unit tests pass. Every Motion route measures zero horizontal overflow at 393 and 1440 in both themes, and all twelve seeded field trees are asserted onto the screen by scripts/motion-fields-check.mjs. One thing left open deliberately: the shipped qualification scorecard's five bands and MOTION_BANDS' four are calibrated differently. The framework's table is now titled as its own guidance rather than the product's verdict, which removes the contradiction on screen. Making the framework's calibration authoritative over the persisted band column is a product decision nobody has made. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
108 lines
8.3 KiB
SQL
108 lines
8.3 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
|
|
ALTER TABLE "qualification_scores" ADD CONSTRAINT "qualification_scores_scored_by_user_id_users_id_fk" FOREIGN KEY ("scored_by_user_id") REFERENCES "public"."users"("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);
|