fimo 0.2.5-staging.14 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/skills/fimo-migration/SKILL.md +32 -3
- package/assets/skills/fimo-migration/references/content.md +38 -0
- package/assets/skills/fimo-migration/references/import-contract.md +4 -3
- package/assets/skills/fimo-migration/references/media.md +9 -1
- package/assets/skills/fimo-migration/references/qa.md +12 -6
- package/assets/skills/fimo-migration/references/workflow.md +26 -6
- package/assets/skills/fimo-migration/scripts/write-migration-report.mjs +84 -10
- package/dist/cli/bundle.json +2 -2
- package/dist/cli/index.js +164 -130
- package/package.json +1 -1
- package/release.json +2 -2
- package/scripts/migration-qa-gate.test.ts +155 -0
- package/templates/react-router/_gitignore +2 -0
- package/templates/react-router/package.json +1 -1
- package/templates/react-router/pnpm-workspace.yaml +4 -0
|
@@ -32,13 +32,21 @@ imports, and QA so any LLM can run the work without silently dropping content.
|
|
|
32
32
|
schemas, forms, media primitives, translations, and routes.
|
|
33
33
|
- Do not report completion until the QA gate passes or every failed check is
|
|
34
34
|
recorded in the migration report.
|
|
35
|
+
- Do not deploy or publish during the migration. Importing content is required
|
|
36
|
+
and uses the data-plane commands (`fimo assets upload`, `fimo schemas push`,
|
|
37
|
+
`fimo entries create`, `fimo forms push`); these do **not** rebuild the live
|
|
38
|
+
site. `fimo deploy` / `fimo publish` rebuild and restart the site — never run
|
|
39
|
+
them as part of the migration loop or to "check" rendering. Verify locally
|
|
40
|
+
(build + local dev). Deployment is a single, final, owner-approved step:
|
|
41
|
+
when the QA gate passes, **propose** it to the owner instead of running it.
|
|
35
42
|
|
|
36
43
|
## Intake Decision
|
|
37
44
|
|
|
38
45
|
1. Read the target project `AGENTS.md`.
|
|
39
46
|
2. If editing or creating a Fimo project, read the project `fimo` skill and the
|
|
40
|
-
`fimo-cli` skill references needed for schemas, entries, forms, assets
|
|
41
|
-
deploy
|
|
47
|
+
`fimo-cli` skill references needed for schemas, entries, forms, and assets.
|
|
48
|
+
Do not run `fimo deploy` / `fimo publish` here — deployment is a final,
|
|
49
|
+
owner-approved step (see Non-Negotiables and the Deployment rule below).
|
|
42
50
|
3. Classify the source:
|
|
43
51
|
- **source-backed**: export folder, source repo, CMS CSV/XML/JSON, media
|
|
44
52
|
folder, downloaded HTML, or platform export. Use source files as truth and
|
|
@@ -113,7 +121,28 @@ Final status must be one of:
|
|
|
113
121
|
import/verify yet.
|
|
114
122
|
|
|
115
123
|
Never use “complete” when CMS entries, media mappings, forms, or route coverage
|
|
116
|
-
are unknown.
|
|
124
|
+
are unknown. Generated seed JSON and media bundled under `public/` are **draft
|
|
125
|
+
state**, not imported content: a migration whose entries were never created in
|
|
126
|
+
Fimo, or whose referenced media was never uploaded to the Fimo media library, is
|
|
127
|
+
`draft-only` (or `complete-with-unresolved` only when each gap is an explicit,
|
|
128
|
+
owner-visible unresolved item). The QA gate enforces this and exits non-zero
|
|
129
|
+
otherwise.
|
|
130
|
+
|
|
131
|
+
## Deployment
|
|
132
|
+
|
|
133
|
+
Deployment is **out of scope for the migration loop** and happens at most once,
|
|
134
|
+
at the very end, with the owner's approval.
|
|
135
|
+
|
|
136
|
+
- Never run `fimo deploy` or `fimo publish` to verify, preview, or "check"
|
|
137
|
+
rendering while migrating. All verification (build, local dev, route render,
|
|
138
|
+
visual comparison) is done locally.
|
|
139
|
+
- Importing content (`fimo assets upload`, `fimo schemas push`,
|
|
140
|
+
`fimo entries create`, `fimo forms push`) is part of the migration and does
|
|
141
|
+
not deploy the site — run those as the import contract requires.
|
|
142
|
+
- After the QA gate passes and the report is written, end by **proposing**
|
|
143
|
+
deployment: tell the owner the migration is ready and that running
|
|
144
|
+
`fimo deploy` once will publish it. Let them decide; do not deploy on their
|
|
145
|
+
behalf unless they explicitly ask in this turn.
|
|
117
146
|
|
|
118
147
|
## URL-Only Migration Contract
|
|
119
148
|
|
|
@@ -2,6 +2,44 @@
|
|
|
2
2
|
|
|
3
3
|
Model CMS content before page implementation.
|
|
4
4
|
|
|
5
|
+
## Detecting Dynamic Collections (no CMS export)
|
|
6
|
+
|
|
7
|
+
When there is no first-party CMS export (most url-only and website-builder
|
|
8
|
+
migrations), dynamic content still exists and must be modeled. It shows up two
|
|
9
|
+
ways:
|
|
10
|
+
|
|
11
|
+
1. **Repeated route groups** - many sibling routes share a path prefix
|
|
12
|
+
(`/blog/*`, `/vr-games/*`, `/locations/*`). Each group is a collection.
|
|
13
|
+
Ignore locale prefixes (`/en-be/`, `/fr/`): a locale segment is never a
|
|
14
|
+
collection name.
|
|
15
|
+
2. **Repeated in-page blocks** - a single page renders an array of
|
|
16
|
+
structurally-identical items: card grids, list rows, tiles, carousels, logo
|
|
17
|
+
walls. Games, locations, team members, testimonials, pricing tiers, FAQs,
|
|
18
|
+
news teasers, and feature lists are almost always collections even when the
|
|
19
|
+
source renders them inside one page.
|
|
20
|
+
|
|
21
|
+
After the source-faithful rebuild, treat **any hardcoded array of 3+ items with
|
|
22
|
+
the same shape** as a collection candidate. Do not leave it hardcoded.
|
|
23
|
+
|
|
24
|
+
For each candidate:
|
|
25
|
+
|
|
26
|
+
- name it as a singular content type with a plural label (`game`/Games,
|
|
27
|
+
`location`/Locations, `newsPost`/News, `testimonial`/Testimonials,
|
|
28
|
+
`pricingPlan`/Pricing). Never name a collection after a locale code or a
|
|
29
|
+
generic page route (no `EnBe`, no `Page`).
|
|
30
|
+
- infer fields from the item shape using the Field Mapping below.
|
|
31
|
+
- push the schema, then create one entry per item.
|
|
32
|
+
- upload each item's image with `fimo assets upload` and store the returned Fimo
|
|
33
|
+
asset URL in the entry's `media` field (never the source URL or a local
|
|
34
|
+
`public/` path).
|
|
35
|
+
- wire the component to render from the collection (Fimo content
|
|
36
|
+
query/primitives) with a seed/empty state. The hardcoded array is removed.
|
|
37
|
+
- record source/generated/imported counts per content type in the manifest.
|
|
38
|
+
|
|
39
|
+
Genuinely one-off blocks (a single hero, the footer) stay static. If unsure
|
|
40
|
+
whether a block is a collection, prefer modeling it as one when items share a
|
|
41
|
+
clear repeated shape and could plausibly be edited or extended.
|
|
42
|
+
|
|
5
43
|
## Field Mapping
|
|
6
44
|
|
|
7
45
|
Use source headers, values, and repeated patterns:
|
|
@@ -29,9 +29,10 @@ Import requirements:
|
|
|
29
29
|
|
|
30
30
|
Acceptance:
|
|
31
31
|
|
|
32
|
-
- `media.total` and `media.
|
|
33
|
-
- referenced media has `targetAssetId
|
|
34
|
-
`unresolved`
|
|
32
|
+
- `media.total` and `media.uploaded/unresolved` counts are in the report
|
|
33
|
+
- referenced media has a Fimo `targetAssetId` or an uploaded Fimo `targetUrl`,
|
|
34
|
+
or is explicitly `unsupported`/`unresolved`. A local `public/` path or the
|
|
35
|
+
original source URL does not count as covered.
|
|
35
36
|
- primary routes render without broken images
|
|
36
37
|
|
|
37
38
|
## CMS Entries
|
|
@@ -37,7 +37,15 @@ sources point to one uploaded asset.
|
|
|
37
37
|
|
|
38
38
|
## Upload And Replacement
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
Referenced media must be uploaded into the Fimo media library with
|
|
41
|
+
`fimo assets upload`. Copying a file under `public/` so the deployed site can
|
|
42
|
+
serve it is **not** a completed import: it has no Fimo asset id/URL, editors
|
|
43
|
+
cannot manage it, and the QA gate treats a referenced-but-not-uploaded asset as
|
|
44
|
+
failing. Either upload it (and record the Fimo target), or mark it `unresolved`
|
|
45
|
+
with an owner action. "Bundled locally, upload later" is not an acceptable
|
|
46
|
+
terminal state for referenced media.
|
|
47
|
+
|
|
48
|
+
After upload, update the manifest with the Fimo target asset ID/URL.
|
|
41
49
|
|
|
42
50
|
When using downloaded files from a Fimo migration pack, copy the recorded
|
|
43
51
|
`localPath` from the media map exactly. Do not reconstruct local filenames from
|
|
@@ -6,9 +6,11 @@ Run this gate before reporting completion.
|
|
|
6
6
|
|
|
7
7
|
- Every intended source route is implemented, ignored with a reason, or unresolved.
|
|
8
8
|
- Every CMS export file is mapped to a schema, ignored with a reason, or unresolved.
|
|
9
|
-
- Source entry counts match
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
- Source entry counts match imported counts (entries created in Fimo), excluding
|
|
10
|
+
documented skips/unresolved. Generated seed JSON alone does not satisfy this.
|
|
11
|
+
- Every media item referenced by routes, CMS entries, SEO, or forms is uploaded to
|
|
12
|
+
the Fimo media library (Fimo asset id/URL) or has an unresolved item. A local
|
|
13
|
+
`public/` copy is not an upload mapping.
|
|
12
14
|
- Every visible form has a Fimo replacement, third-party plan, or unsupported note.
|
|
13
15
|
- SEO title, description, canonical URL, share image, slugs, dates, authors,
|
|
14
16
|
categories, tags, redirects, and locales are preserved when present.
|
|
@@ -43,9 +45,13 @@ Record:
|
|
|
43
45
|
|
|
44
46
|
Counts must be written into `.fimo/migration/manifest.json`.
|
|
45
47
|
|
|
46
|
-
Media, entries, and forms are blocking checks.
|
|
47
|
-
|
|
48
|
-
`draft-only
|
|
48
|
+
Media, entries, and forms are blocking checks. Entries generated as seed JSON but
|
|
49
|
+
not created in Fimo, and media bundled under `public/` but not uploaded to the
|
|
50
|
+
Fimo media library, are NOT covered: the gate fails to `draft-only` unless each
|
|
51
|
+
gap has an explicit, owner-visible unresolved item. The bundled report script
|
|
52
|
+
enforces this - `cms-counts` requires imported (or skipped/unresolved) entries,
|
|
53
|
+
`schemas-pushed` requires the content type registered, and `media-covered`
|
|
54
|
+
requires a Fimo asset id/URL.
|
|
49
55
|
|
|
50
56
|
## Report
|
|
51
57
|
|
|
@@ -78,14 +78,18 @@ Every item needs one of these statuses:
|
|
|
78
78
|
|
|
79
79
|
Before writing page code:
|
|
80
80
|
|
|
81
|
-
1.
|
|
82
|
-
|
|
81
|
+
1. Detect dynamic collections from both repeated route groups AND repeated
|
|
82
|
+
in-page blocks. A hardcoded array of 3+ structurally-identical items (cards,
|
|
83
|
+
list rows, tiles) is a collection, not static markup. Ignore locale prefixes
|
|
84
|
+
when naming collections.
|
|
85
|
+
2. Map each CMS export or inferred collection to a Fimo schema or unresolved item.
|
|
86
|
+
3. Preserve stable source IDs, slugs, dates, SEO, authors, categories, tags,
|
|
83
87
|
locales, and publish status.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
4. Decide which relations are real. Do not invent references from vague text.
|
|
89
|
+
5. Build seed/import entries with source IDs and media references preserved.
|
|
90
|
+
6. Record source count and planned import count for each content type.
|
|
87
91
|
|
|
88
|
-
Use `references/content.md
|
|
92
|
+
Use `references/content.md`, including "Detecting Dynamic Collections".
|
|
89
93
|
|
|
90
94
|
## Phase 4: Media And Forms
|
|
91
95
|
|
|
@@ -132,6 +136,9 @@ Rules:
|
|
|
132
136
|
- Schema-driven media renders with Fimo media primitives, not raw source URLs.
|
|
133
137
|
- CMS pages must have matching `src/schemas/*.json` before generated clients or
|
|
134
138
|
queries are used.
|
|
139
|
+
- Repeated dynamic items (games, locations, posts, testimonials, pricing) render
|
|
140
|
+
from CMS collections, not hardcoded arrays. A page left with a hardcoded array
|
|
141
|
+
of collection-shaped items is an incomplete migration, not a finished one.
|
|
135
142
|
- Generated `.ts` schema/form clients are not edited manually.
|
|
136
143
|
- Unsupported source behavior is replaced with a Fimo-native equivalent or shown
|
|
137
144
|
as unresolved, never hidden behind broken UI.
|
|
@@ -171,3 +178,16 @@ node <this-skill>/scripts/write-migration-report.mjs <project-root>
|
|
|
171
178
|
|
|
172
179
|
The script exits non-zero when required coverage checks fail. That is a prompt
|
|
173
180
|
to continue the migration or document the gaps, not a reason to hide the report.
|
|
181
|
+
|
|
182
|
+
All checks in this phase are **local** (build, local dev, route render, visual
|
|
183
|
+
comparison). Do not run `fimo deploy` / `fimo publish` to verify anything.
|
|
184
|
+
|
|
185
|
+
## Phase 9: Propose Deployment
|
|
186
|
+
|
|
187
|
+
Deployment is not part of the migration loop and is never run to preview or
|
|
188
|
+
verify. Once Phase 8 passes:
|
|
189
|
+
|
|
190
|
+
1. Do not run `fimo deploy` or `fimo publish` yourself.
|
|
191
|
+
2. End by proposing it: tell the owner the migration is ready and that a single
|
|
192
|
+
`fimo deploy` will publish it.
|
|
193
|
+
3. Deploy only if the owner explicitly asks in this turn, and then only once.
|
|
@@ -91,14 +91,21 @@ function buildChecks(manifest, projectRoot) {
|
|
|
91
91
|
addCheck(
|
|
92
92
|
checks,
|
|
93
93
|
'cms-counts',
|
|
94
|
-
'CMS
|
|
94
|
+
'CMS entries are imported into Fimo (or explicitly skipped/unresolved) per content type',
|
|
95
95
|
contentCountsPass(manifest),
|
|
96
96
|
contentCountStats(manifest)
|
|
97
97
|
);
|
|
98
|
+
addCheck(
|
|
99
|
+
checks,
|
|
100
|
+
'schemas-pushed',
|
|
101
|
+
'Schemas for content with entries are pushed to Fimo before entries',
|
|
102
|
+
schemasPushedPass(manifest),
|
|
103
|
+
schemaPushStats(manifest)
|
|
104
|
+
);
|
|
98
105
|
addCheck(
|
|
99
106
|
checks,
|
|
100
107
|
'media-covered',
|
|
101
|
-
'Referenced media
|
|
108
|
+
'Referenced media is uploaded to Fimo (Fimo asset id/URL) or marked unresolved',
|
|
102
109
|
mediaCoveragePass(manifest),
|
|
103
110
|
mediaStats(manifest)
|
|
104
111
|
);
|
|
@@ -238,7 +245,8 @@ function mediaSummary(manifest) {
|
|
|
238
245
|
if (media.length === 0) return '- No media recorded.';
|
|
239
246
|
return [
|
|
240
247
|
`- Media items: ${media.length}`,
|
|
241
|
-
`-
|
|
248
|
+
`- Uploaded to Fimo: ${media.filter((item) => isFimoAssetTarget(item)).length}`,
|
|
249
|
+
`- Bundled locally (not uploaded): ${media.filter((item) => isLocallyBundled(item)).length}`,
|
|
242
250
|
`- Unresolved: ${media.filter((item) => item.status === 'unresolved' || item.uploadError).length}`,
|
|
243
251
|
`- Referenced: ${media.filter((item) => asArray(item.referencedBy).length > 0).length}`,
|
|
244
252
|
].join('\n');
|
|
@@ -310,16 +318,52 @@ function coverageStats(items) {
|
|
|
310
318
|
}
|
|
311
319
|
|
|
312
320
|
function contentCountsPass(manifest) {
|
|
321
|
+
// Hard gate: generating seed JSON is not enough. A batch with source entries
|
|
322
|
+
// passes only when those entries were actually created in Fimo (importedCount),
|
|
323
|
+
// were explicitly skipped, or the whole batch is deliberately marked
|
|
324
|
+
// unresolved/unsupported/ignored. `generatedCount` no longer satisfies this.
|
|
313
325
|
return asArray(manifest.entries).every((batch) => {
|
|
314
326
|
const source = numberOr(batch.sourceCount, 0);
|
|
315
|
-
|
|
327
|
+
if (source === 0) return true;
|
|
316
328
|
const imported = numberOr(batch.importedCount, 0);
|
|
317
329
|
const skipped = asArray(batch.skipped).length;
|
|
318
|
-
if (
|
|
319
|
-
return
|
|
330
|
+
if (imported + skipped >= source) return true;
|
|
331
|
+
return ['unresolved', 'unsupported', 'ignored'].includes(batch.status);
|
|
320
332
|
});
|
|
321
333
|
}
|
|
322
334
|
|
|
335
|
+
function schemasPushedPass(manifest) {
|
|
336
|
+
const requiredTypes = entryBatchTypesNeedingSchema(manifest);
|
|
337
|
+
if (requiredTypes.size === 0) return true;
|
|
338
|
+
const byName = new Map(asArray(manifest.contentTypes).map((type) => [type.name ?? type.uid, type]));
|
|
339
|
+
for (const name of requiredTypes) {
|
|
340
|
+
const type = byName.get(name);
|
|
341
|
+
if (!type || !['imported', 'verified'].includes(type.status)) return false;
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function schemaPushStats(manifest) {
|
|
347
|
+
const requiredTypes = entryBatchTypesNeedingSchema(manifest);
|
|
348
|
+
const byName = new Map(asArray(manifest.contentTypes).map((type) => [type.name ?? type.uid, type]));
|
|
349
|
+
const pushed = [...requiredTypes].filter((name) => ['imported', 'verified'].includes(byName.get(name)?.status));
|
|
350
|
+
return {
|
|
351
|
+
requiredTypes: requiredTypes.size,
|
|
352
|
+
pushed: pushed.length,
|
|
353
|
+
missing: [...requiredTypes].filter((name) => !pushed.includes(name)),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function entryBatchTypesNeedingSchema(manifest) {
|
|
358
|
+
return new Set(
|
|
359
|
+
asArray(manifest.entries)
|
|
360
|
+
.filter((batch) => numberOr(batch.sourceCount, 0) > 0)
|
|
361
|
+
.filter((batch) => !['unresolved', 'unsupported', 'ignored'].includes(batch.status))
|
|
362
|
+
.map((batch) => batch.contentType ?? batch.type ?? batch.uid)
|
|
363
|
+
.filter(isNonEmptyString)
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
323
367
|
function contentCountStats(manifest) {
|
|
324
368
|
const batches = asArray(manifest.entries);
|
|
325
369
|
return {
|
|
@@ -332,25 +376,51 @@ function contentCountStats(manifest) {
|
|
|
332
376
|
}
|
|
333
377
|
|
|
334
378
|
function mediaCoveragePass(manifest) {
|
|
379
|
+
// Hard gate: a referenced asset bundled locally under public/ does not count as
|
|
380
|
+
// covered. It must be uploaded to the Fimo media library (target asset id, or an
|
|
381
|
+
// absolute upload URL distinct from the source) or be explicitly unresolved.
|
|
335
382
|
return asArray(manifest.media).every((item) => {
|
|
336
383
|
const referenced = asArray(item.referencedBy).length > 0;
|
|
337
384
|
if (!referenced) return true;
|
|
338
|
-
return
|
|
339
|
-
item.targetAssetId || item.targetUrl || ['ignored', 'unsupported', 'unresolved'].includes(item.status)
|
|
340
|
-
);
|
|
385
|
+
return isFimoAssetTarget(item) || ['ignored', 'unsupported', 'unresolved'].includes(item.status);
|
|
341
386
|
});
|
|
342
387
|
}
|
|
343
388
|
|
|
389
|
+
function isFimoAssetTarget(item) {
|
|
390
|
+
if (!item) return false;
|
|
391
|
+
if (isNonEmptyString(item.targetAssetId)) return true;
|
|
392
|
+
return isUploadedTargetUrl(item);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function isUploadedTargetUrl(item) {
|
|
396
|
+
const url = item.targetUrl;
|
|
397
|
+
if (!isNonEmptyString(url)) return false;
|
|
398
|
+
const trimmed = url.trim();
|
|
399
|
+
// Reject local/site-relative bundles: /assets/..., public/..., ./..., bare filenames.
|
|
400
|
+
if (!/^https?:\/\//i.test(trimmed)) return false;
|
|
401
|
+
// Reject a target that merely echoes the source location (nothing was uploaded).
|
|
402
|
+
const sources = [item.sourceUrl, item.sourcePath, item.source].filter(isNonEmptyString).map((value) => value.trim());
|
|
403
|
+
if (sources.includes(trimmed)) return false;
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
344
407
|
function mediaStats(manifest) {
|
|
345
408
|
const media = asArray(manifest.media);
|
|
346
409
|
return {
|
|
347
410
|
total: media.length,
|
|
348
411
|
referenced: media.filter((item) => asArray(item.referencedBy).length > 0).length,
|
|
349
|
-
|
|
412
|
+
uploaded: media.filter((item) => isFimoAssetTarget(item)).length,
|
|
413
|
+
bundledLocally: media.filter((item) => isLocallyBundled(item)).length,
|
|
350
414
|
unresolved: media.filter((item) => item.status === 'unresolved' || item.uploadError).length,
|
|
351
415
|
};
|
|
352
416
|
}
|
|
353
417
|
|
|
418
|
+
function isLocallyBundled(item) {
|
|
419
|
+
if (!item || isFimoAssetTarget(item)) return false;
|
|
420
|
+
const candidates = [item.targetUrl, item.targetPath, item.localPath].filter(isNonEmptyString);
|
|
421
|
+
return candidates.some((value) => !/^https?:\/\//i.test(value.trim()));
|
|
422
|
+
}
|
|
423
|
+
|
|
354
424
|
function formsCoveragePass(manifest) {
|
|
355
425
|
return asArray(manifest.forms).every((form) => {
|
|
356
426
|
if (form.classification === 'fimo-native') return Boolean(form.targetFiles?.length) || terminalStatus(form.status);
|
|
@@ -445,6 +515,10 @@ function asArray(value) {
|
|
|
445
515
|
return Array.isArray(value) ? value : [];
|
|
446
516
|
}
|
|
447
517
|
|
|
518
|
+
function isNonEmptyString(value) {
|
|
519
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
520
|
+
}
|
|
521
|
+
|
|
448
522
|
function numberOr(value, fallback) {
|
|
449
523
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
450
524
|
}
|
package/dist/cli/bundle.json
CHANGED