Fix twenty findings from the Motion review
CI / verify (push) Successful in 4m47s
CI / publish (push) Failing after 3s

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>
This commit is contained in:
2026-08-17 19:08:55 -07:00
parent 376ef3d597
commit 15c72ade1c
16 changed files with 1281 additions and 66 deletions
+1
View File
@@ -98,6 +98,7 @@ ALTER TABLE "engagement_artifacts" ADD CONSTRAINT "engagement_artifacts_promoted
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
+14 -2
View File
@@ -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, sql } from 'drizzle-orm';
import { eq, inArray, like, or, sql } from 'drizzle-orm';
import { unstampDemoActivity } from './activities';
import {
accounts,
@@ -96,11 +96,23 @@ export async function clear(context: DemoContext): Promise<void> {
* 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.
*
* Found by parentage as well as by prefix, because the cascade off the demo
* deals takes a wider set than the prefix does: an engagement opened on a
* demo deal through the app carries whatever summary its author typed, or
* none. Every one of those the prefix scan missed would be deleted anyway a
* few lines below, silently, with its artefacts' borrowed `usage_count`
* never given back — and a starter template left above zero can never be
* edited again.
*/
const demoEngagements = await db
.select({ id: engagements.id })
.from(engagements)
.where(like(engagements.summary, `${prefix}%`));
.where(
demandDealIds.length > 0
? or(like(engagements.summary, `${prefix}%`), inArray(engagements.demandDealId, demandDealIds))
: like(engagements.summary, `${prefix}%`),
);
const engagementIds = demoEngagements.map((engagement) => engagement.id);
if (engagementIds.length > 0) {
/*
File diff suppressed because one or more lines are too long