2f32186d22
The markdown parser was in the eager entry chunk. `manualChunks` in its object form does not leave an unlisted vendor package to Vite's async splitting, so react-markdown was hoisted into the entry even though its only importers are lazy routes — 327.70 kB gzip against a 314 kB baseline, on the one download every route pays for. Naming it as its own chunk puts it back behind the Motion pages and takes the entry to 282.21 kB, below where it was before Motion existed. The starter library could never be improved. Seeding was insert-only, so a deployment seeded in August was frozen on August's wording for ever with no upgrade path short of editing production rows by hand — for a feature whose entire premise is that the library gets better. A second run now refreshes a starter row, but only while it is still ours: `is_system`, `usage_count = 0` and no owner. That is the same condition §7a already enforces on the API, so a template an engagement was cut from is left alone and reported by name rather than silently overwritten. The refresh was not idempotent, and the seed lied about it. `jsonb` does not preserve key order — Postgres sorts keys by length then bytewise — so comparing `JSON.stringify(stored)` against `JSON.stringify(authored)` marked every template as changed on every run, and the seed rewrote nine rows each time while reporting itself clean. Comparison is now canonical. Found by running the seed three times and reading the counts. `motion-overflow-check.mjs` measured less than it claimed. It seeded `pig.sidebar` and `pig.piggy.dock`, neither of which anything reads (the keys are `pig.sidebarOpen` and `pig.piggyDockOpen`), so the layout it pinned was whatever the last run left. Its dark pass set `colorScheme` only, and the appearance preference is stored server-side and adopted after hydration, so the dark pass measured the light palette a beat after first paint. It now rewrites the profile response as `screenshots.mjs` does, asserts the rendered `data-theme`, and fails a page that renders almost no text — a page that throws inside its own body otherwise measures zero overflow and passes. The stage rail rendered "1 templates", in the visible label and in every aria-label. Singular and plural are now both passed. Also normalised `artifact` to `artefact` in the seeded prose, which had drifted American in the playbook. The `artifacts` field key is untouched: FieldsView reads it, and already labels it in British.
272 lines
11 KiB
TypeScript
272 lines
11 KiB
TypeScript
/**
|
|
* The starter library — twelve authored templates, shipped with the product.
|
|
*
|
|
* These are **not** demo data and carry no `DEMO — ` prefix. They are written
|
|
* content in the same class as `docs/learn-scripts.md`: a customer who
|
|
* self-hosts PIG gets them, uses them against real deals, and promotes their
|
|
* own versions back over the top. That is why they seed `isSystem: true`,
|
|
* `visibility: 'shared'`, `version: 1` and `ownerUserId: null` — a shared
|
|
* system row belongs to the deployment rather than to whoever ran the seed,
|
|
* and version 1 is the base of a lineage that a customer's promotions extend.
|
|
*
|
|
* The prose lives in JSON beside this file rather than in a TypeScript literal
|
|
* because the bodies are long-form markdown — the shortest is 7kB and the
|
|
* longest 24kB — and a 200kB module of backtick strings is unreviewable and
|
|
* unmergeable by two people at once. They are imported statically with an
|
|
* import attribute rather than read with `readdir`, which was tried first: a
|
|
* static import makes a missing or renamed file a typecheck failure instead of
|
|
* a seed that quietly ships eleven templates. Both `tsx` and `tsc` were run
|
|
* against this to confirm the attribute syntax is understood by each, since
|
|
* the server runs the TypeScript directly.
|
|
*
|
|
* Idempotency is keyed on `(slug, version)`, which is enforceable **only**
|
|
* because of `motion_templates_slug_version_key`. AGENTS.md §5 is blunt about
|
|
* what `onConflictDoNothing` does without a constraint to fire on — it silently
|
|
* duplicated seed data here twice — so `assertNoDuplicates` below checks the
|
|
* outcome rather than trusting the schema, and CI counts this table in its
|
|
* idempotency gate alongside `contacts`.
|
|
*
|
|
* A second run REFRESHES a starter row rather than skipping it, but only while
|
|
* that row is still ours: `is_system`, nobody has forked or instantiated it
|
|
* (`usage_count = 0`) and it has no owner. Insert-only was the first version and
|
|
* is wrong for the one thing this library is for — the content improves, and a
|
|
* deployment seeded in August would otherwise be frozen on August's wording for
|
|
* ever, with no upgrade path short of hand-editing production rows. The
|
|
* pristine test is what keeps that from becoming a write over somebody's work:
|
|
* the moment a template has been used, §7a says it is never edited in place,
|
|
* and this respects that with the same condition the API enforces.
|
|
*/
|
|
import { DEMAND_STAGES, isMotionKind, type DemandStage, type MotionKind } from '@pig/core';
|
|
import { and, eq, inArray } from 'drizzle-orm';
|
|
import type { Database } from '../../client';
|
|
import { motionTemplates } from '../../schema/index';
|
|
|
|
import caseStudyFrame from './case-study-frame.json' with { type: 'json' };
|
|
import dataAndSecurityBrief from './data-and-security-brief.json' with { type: 'json' };
|
|
import postTrainingPoc from './post-training-poc.json' with { type: 'json' };
|
|
import pricingAndPackaging from './pricing-and-packaging.json' with { type: 'json' };
|
|
import procurementAndBudgetPath from './procurement-and-budget-path.json' with { type: 'json' };
|
|
import productionReadiness from './production-readiness.json' with { type: 'json' };
|
|
import proposalBlocks from './proposal-blocks.json' with { type: 'json' };
|
|
import referenceArchitectures from './reference-architectures.json' with { type: 'json' };
|
|
import strategicDeploymentPlaybook from './strategic-deployment-playbook.json' with { type: 'json' };
|
|
import technicalDiscovery from './technical-discovery.json' with { type: 'json' };
|
|
import technicalNarratives from './technical-narratives.json' with { type: 'json' };
|
|
import trainabilityQualification from './trainability-qualification.json' with { type: 'json' };
|
|
|
|
/**
|
|
* The authored shape. `kind` and `stage` are `string` here and narrowed at the
|
|
* boundary below, because `resolveJsonModule` infers `string` for a JSON string
|
|
* and there is no honest way to tell TypeScript otherwise without a cast that
|
|
* would also swallow a genuine typo in the file.
|
|
*/
|
|
interface StarterTemplate {
|
|
readonly kind: string;
|
|
readonly slug: string;
|
|
readonly title: string;
|
|
readonly summary: string;
|
|
readonly stage: string;
|
|
readonly body: string;
|
|
readonly fields: Record<string, unknown>;
|
|
/** Guidance for whoever runs it. Folded into `fields` — see `toRow`. */
|
|
readonly notes: string;
|
|
}
|
|
|
|
const STARTER_LIBRARY: readonly StarterTemplate[] = [
|
|
technicalDiscovery,
|
|
dataAndSecurityBrief,
|
|
trainabilityQualification,
|
|
referenceArchitectures,
|
|
technicalNarratives,
|
|
postTrainingPoc,
|
|
proposalBlocks,
|
|
pricingAndPackaging,
|
|
procurementAndBudgetPath,
|
|
productionReadiness,
|
|
caseStudyFrame,
|
|
strategicDeploymentPlaybook,
|
|
];
|
|
|
|
const isDemandStage = (value: string): value is DemandStage =>
|
|
(DEMAND_STAGES as readonly string[]).includes(value);
|
|
|
|
/**
|
|
* `notes` has no column, deliberately.
|
|
*
|
|
* It is guidance for the person running the template — when to reach for it,
|
|
* what it is not for — rather than a field the artifact renders, so a column
|
|
* would put it on every engagement artifact's editor for no reason. Keeping it
|
|
* inside `fields` means promotion carries it forward with the rest of the
|
|
* structured payload without any special handling in the promote path.
|
|
*/
|
|
/*
|
|
* Key order is not preserved by `jsonb`: Postgres stores an object with its
|
|
* keys sorted by length and then bytewise, so the value that comes back is
|
|
* rarely the value that went in. A plain `JSON.stringify` comparison therefore
|
|
* reported every starter template as changed on every run, and the seed
|
|
* rewrote nine rows each time while claiming to be idempotent. Found by
|
|
* running it twice and reading the count, not by reading the code.
|
|
*/
|
|
function canonical(value: unknown): string {
|
|
const order = (node: unknown): unknown => {
|
|
if (Array.isArray(node)) return node.map(order);
|
|
if (node && typeof node === 'object') {
|
|
return Object.fromEntries(
|
|
Object.keys(node as Record<string, unknown>)
|
|
.sort()
|
|
.map((key) => [key, order((node as Record<string, unknown>)[key])]),
|
|
);
|
|
}
|
|
return node;
|
|
};
|
|
return JSON.stringify(order(value));
|
|
}
|
|
|
|
function toRow(
|
|
template: StarterTemplate,
|
|
kind: MotionKind,
|
|
stage: DemandStage,
|
|
): typeof motionTemplates.$inferInsert {
|
|
return {
|
|
kind,
|
|
stage,
|
|
slug: template.slug,
|
|
version: 1,
|
|
title: template.title,
|
|
summary: template.summary,
|
|
body: template.body,
|
|
fields: { ...template.fields, notes: template.notes },
|
|
visibility: 'shared',
|
|
ownerUserId: null,
|
|
isSystem: true,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Confirm the outcome, not the constraint.
|
|
*
|
|
* Reading `pg_constraint` would prove the unique index exists; counting the
|
|
* rows proves the clause did its job, which is the thing that has actually
|
|
* gone wrong here before. It throws rather than warns: a duplicated starter
|
|
* library is a corrupt library, and every subsequent run would double it again.
|
|
*
|
|
* Scoped to version 1, and found by running it: the demo book promotes an
|
|
* artifact into `case-study-frame` v2, so counting every row for these slugs
|
|
* reported the loop working as a duplicated seed. Version 1 is the only row
|
|
* this loader writes and therefore the only one it may assert about.
|
|
*/
|
|
async function assertNoDuplicates(db: Database, slugs: readonly string[]): Promise<number> {
|
|
const rows = await db
|
|
.select({ slug: motionTemplates.slug })
|
|
.from(motionTemplates)
|
|
.where(and(inArray(motionTemplates.slug, [...slugs]), eq(motionTemplates.version, 1)));
|
|
|
|
if (rows.length > slugs.length) {
|
|
const counts = new Map<string, number>();
|
|
for (const row of rows) counts.set(row.slug, (counts.get(row.slug) ?? 0) + 1);
|
|
const duplicated = [...counts.entries()].filter(([, count]) => count > 1).map(([slug]) => slug);
|
|
throw new Error(
|
|
`The starter library has duplicate rows for: ${duplicated.join(', ')}. ` +
|
|
'The unique constraint motion_templates_slug_version_key is missing or the ' +
|
|
'conflict target no longer matches it — see AGENTS.md §5.',
|
|
);
|
|
}
|
|
|
|
return rows.length;
|
|
}
|
|
|
|
export async function seedMotionLibrary(
|
|
db: Database,
|
|
): Promise<{
|
|
total: number;
|
|
added: number;
|
|
refreshed: number;
|
|
held: readonly string[];
|
|
rejected: readonly string[];
|
|
}> {
|
|
const rejected: string[] = [];
|
|
const held: string[] = [];
|
|
let added = 0;
|
|
let refreshed = 0;
|
|
|
|
for (const template of STARTER_LIBRARY) {
|
|
/*
|
|
* A bad `kind` or `stage` is skipped, not thrown on. These files are
|
|
* authored by hand and the CHECK constraints would reject the row anyway —
|
|
* but as an aborted transaction partway through the base seed, taking the
|
|
* eleven good templates and everything after them down with it. Reporting
|
|
* the one bad file by name is more useful and leaves the deployment usable.
|
|
*/
|
|
if (!isMotionKind(template.kind) || !isDemandStage(template.stage)) {
|
|
rejected.push(`${template.slug} (kind=${template.kind}, stage=${template.stage})`);
|
|
continue;
|
|
}
|
|
|
|
const row = toRow(template, template.kind, template.stage);
|
|
|
|
const [existing] = await db
|
|
.select({
|
|
id: motionTemplates.id,
|
|
isSystem: motionTemplates.isSystem,
|
|
usageCount: motionTemplates.usageCount,
|
|
ownerUserId: motionTemplates.ownerUserId,
|
|
title: motionTemplates.title,
|
|
summary: motionTemplates.summary,
|
|
body: motionTemplates.body,
|
|
fields: motionTemplates.fields,
|
|
kind: motionTemplates.kind,
|
|
stage: motionTemplates.stage,
|
|
})
|
|
.from(motionTemplates)
|
|
.where(and(eq(motionTemplates.slug, template.slug), eq(motionTemplates.version, 1)))
|
|
.limit(1);
|
|
|
|
if (!existing) {
|
|
const [created] = await db
|
|
.insert(motionTemplates)
|
|
.values(row)
|
|
.onConflictDoNothing({ target: [motionTemplates.slug, motionTemplates.version] })
|
|
.returning({ id: motionTemplates.id });
|
|
if (created) added += 1;
|
|
continue;
|
|
}
|
|
|
|
const pristine =
|
|
existing.isSystem && existing.usageCount === 0 && existing.ownerUserId === null;
|
|
const changed =
|
|
existing.title !== row.title ||
|
|
existing.summary !== row.summary ||
|
|
existing.body !== row.body ||
|
|
existing.kind !== row.kind ||
|
|
existing.stage !== row.stage ||
|
|
canonical(existing.fields) !== canonical(row.fields);
|
|
|
|
if (pristine && changed) {
|
|
await db
|
|
.update(motionTemplates)
|
|
.set({
|
|
title: row.title,
|
|
summary: row.summary,
|
|
body: row.body,
|
|
fields: row.fields,
|
|
kind: row.kind,
|
|
stage: row.stage,
|
|
updatedAt: new Date(),
|
|
})
|
|
.where(eq(motionTemplates.id, existing.id));
|
|
refreshed += 1;
|
|
} else if (changed) {
|
|
// Reported rather than forced: the row has been used or adopted, so
|
|
// overwriting it would edit a template a live engagement was cut from.
|
|
held.push(template.slug);
|
|
}
|
|
}
|
|
|
|
const total = await assertNoDuplicates(
|
|
db,
|
|
STARTER_LIBRARY.map((template) => template.slug),
|
|
);
|
|
|
|
return { total, added, refreshed, held, rejected };
|
|
}
|