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>
86 lines
3.6 KiB
JavaScript
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);
|