fimo 0.2.5-staging.14 → 0.2.5-staging.15

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.
@@ -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, and
41
- deploy commands.
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.imported/unresolved` counts are in the report
33
- - referenced media has `targetAssetId`, `targetUrl`, `unsupported`, or
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
- After upload, update the manifest with target asset ID/URL.
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 generated/imported counts, excluding documented skips.
10
- - Every media item referenced by routes, CMS entries, SEO, or forms has an upload
11
- mapping or unresolved item.
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. If any of these are present in
47
- the source but have unknown imported counts or no unresolved item, report
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. Map each CMS export or inferred collection to a Fimo schema or unresolved item.
82
- 2. Preserve stable source IDs, slugs, dates, SEO, authors, categories, tags,
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
- 3. Decide which relations are real. Do not invent references from vague text.
85
- 4. Build seed/import entries with source IDs and media references preserved.
86
- 5. Record source count and planned import count for each content type.
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 generated/imported counts match planned source counts',
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 has upload mapping or unresolved status',
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
- `- Imported: ${media.filter((item) => item.status === 'imported' || item.targetAssetId || item.targetUrl).length}`,
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
- const generated = numberOr(batch.generatedCount, 0);
327
+ if (source === 0) return true;
316
328
  const imported = numberOr(batch.importedCount, 0);
317
329
  const skipped = asArray(batch.skipped).length;
318
- if (source === 0) return true;
319
- return generated + skipped >= source || imported + skipped >= source || terminalStatus(batch.status);
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 Boolean(
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
- imported: media.filter((item) => item.targetAssetId || item.targetUrl || item.status === 'imported').length,
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
  }
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "bundled": true,
3
3
  "bundler": "esbuild",
4
- "bundledAt": "2026-06-26T17:21:55.057Z",
5
- "cliVersion": "0.2.5-staging.14",
4
+ "bundledAt": "2026-06-30T07:17:47.484Z",
5
+ "cliVersion": "0.2.5-staging.15",
6
6
  "external": [
7
7
  "oxc-parser",
8
8
  "fsevents"