Files
pig/scripts/motion-fields-check.mjs
karti 15c72ade1c
CI / verify (push) Successful in 4m47s
CI / publish (push) Failing after 3s
Fix twenty findings from the Motion review
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>
2026-08-17 19:08:55 -07:00

86 lines
3.6 KiB
JavaScript

/*
* Does every seeded template's `fields` actually reach the screen?
*
* node scripts/motion-fields-check.mjs # PIG_WEB_URL=http://127.0.0.1:8975
*
* Three of the twelve starter templates shipped with `fields` shapes no renderer
* in `FieldsView` read — `decisions`, `blockingSet`, `checks`, `steps` and the
* rest — so about forty authored records rendered as no DOM at all, in the
* library and again on the engagement that instantiated them. Nothing failed:
* each renderer returned null for a key set it did not recognise, and a template
* page that is header-plus-body looks like a template that was written that way.
*
* So the check is not "does the page load". It samples the actual authored
* strings out of the seed JSON and asserts they are present in the rendered
* text, which is the only claim that distinguishes a rendered field from a
* dropped one.
*/
import { chromium } from 'playwright';
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
const BASE = process.env.PIG_WEB_URL ?? 'http://127.0.0.1:8975';
const SEEDS = 'packages/db/src/seed/motion';
/** Every string of real prose in a fields tree, longest first. */
function strings(node, out = []) {
if (typeof node === 'string') out.push(node);
else if (Array.isArray(node)) node.forEach((n) => strings(n, out));
else if (node && typeof node === 'object') Object.values(node).forEach((n) => strings(n, out));
return out;
}
const templates = readdirSync(SEEDS)
.filter((f) => f.endsWith('.json'))
.map((f) => JSON.parse(readFileSync(join(SEEDS, f), 'utf8')))
.filter((t) => t.fields && Object.keys(t.fields).length > 0);
const live = await fetch(`${BASE}/api/motion/templates?all=1`).then((r) => r.json());
const bySlug = new Map(live.templates.map((t) => [t.slug, t]));
const browser = await chromium.launch({ channel: 'chrome' });
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
let failures = 0;
for (const template of templates) {
const row = bySlug.get(template.slug);
if (!row) {
console.log(`SKIP ${template.slug}: not in the seeded library`);
continue;
}
// Long strings only: a short one ("Owner", "Yes") can appear in the chrome by
// coincidence and would make this pass on a page that dropped every field.
const sample = strings(template.fields)
.filter((s) => s.length > 45)
.sort((a, b) => b.length - a.length)
.slice(0, 6);
if (sample.length === 0) {
console.log(`SKIP ${template.slug}: no prose long enough to sample`);
continue;
}
const page = await ctx.newPage();
await page.goto(`${BASE}/motion/library/${row.id}`, { waitUntil: 'networkidle' });
await page.waitForTimeout(900);
const text = await page.evaluate(() => document.body.innerText);
// The markdown body is rendered too, so a string that also appears there
// would not prove the FIELDS reached the screen. Those are excluded.
const fieldsOnly = sample.filter((s) => !template.body.includes(s));
const checking = fieldsOnly.length ? fieldsOnly : sample;
const missing = checking.filter((s) => !text.includes(s));
if (missing.length) {
failures++;
console.log(`FAIL ${template.slug} (${template.kind}): ${missing.length}/${checking.length} authored strings absent`);
console.log(` e.g. "${missing[0].slice(0, 90)}…"`);
} else {
console.log(`ok ${template.slug} (${template.kind}): ${checking.length} authored strings on screen`);
}
await page.close();
}
await browser.close();
console.log(failures === 0 ? '\nevery seeded fields tree reaches the screen' : `\n${failures} template(s) render authored content nowhere`);
process.exit(failures === 0 ? 0 : 1);