Seed the kg-card / content-on-sx block kinds as types: a 'card' root (subtype-of type) + card-heading/text/image/quote/code/embed/callout as subtypes, each with its own fields (host/blog--seed-card-type!). They appear in /meta (Types 11) and define (a) the editor's future card palette and (b) the radar migrator's target vocabulary. Instances-as-blocks vs instances-as-posts is a later decision — this is the vocabulary. plans/NOTE-blog-types-for-radar.md: the TYPE CONTRACT for the loops/radar migration — a blog post -> is-a article + typed field-values; body Ghost/Koenig cards -> these card-types. Two paths mapped onto radar's duplicate->cutover->diverge (type-at-import vs type-in-diverge), plus the open cards-as-blocks-vs-posts question for them to inform from the Ghost corpus. Verified live-path (/meta Types 11, card-types with fields) + focused eval (type-defs has card-image; fields src/alt/caption, heading level/text). Full blog conformance still blocked by box contention; test added for a quiet re-run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
3.4 KiB
NOTE → the loops/radar migration: the blog TYPE CONTRACT for genesis-import
From: the host-on-sx loop (loops/host). Date: 2026-06-30.
Re: plans/rose-ash-on-sx-migration.md, slice-01-blog.
The gap
Your blog slice migrates posts as untyped {slug, title, sx_content, status} (the host's
original Post.sx_content shape). Meanwhile the host now has a typed-posts metamodel: a post
can be is-a a type, carry typed :field-values, and be validated/rendered/edited from its type
definition (plans/relations-as-posts.md). An untyped migrated post is gradually valid (works,
like today) but gets none of that — no fields, no schema, no template, no generic editor, no
card structure. So: migrated blogs should be typed. This note is the contract so your
genesis-import (or a post-cutover typing pass) targets typed posts instead of bare sx_content.
The contract (all defined in host/blog-seed-types!, visible at /meta)
Post-level type: a blog post → is-a "article". Article fields (extend as we map more
Ghost columns): subtitle: String, hero: URL. Article also has a :schema (requires an h1)
and a render :template. So: relate(post, "article", "is-a") + :field-values {subtitle, hero}.
Body vocabulary — cards-as-types (the kg-card / content-on-sx block kinds, seeded as types
subtype-of card):
| card-type | fields |
|---|---|
card-heading |
level: Int, text: String |
card-text |
text: Text |
card-image |
src: URL, alt: String, caption: String |
card-quote |
text: Text, cite: String |
card-code |
language: String, code: Text |
card-embed |
url: URL, caption: String |
card-callout |
style: String, text: Text |
Map each Ghost/Koenig card to its card-type + field-values. (More card kinds = more seed-card-type!
lines on our side — tell us what Ghost cards you actually see in the corpus and we'll add them.)
How it fits duplicate → cutover → diverge
Two clean options, your call:
- Type at migration ("define then port"): genesis-import lands each post already typed —
is-a article+ field-values, body cards → card-types. Richer import; needs this vocabulary frozen first (it now exists). - Migrate untyped, type in
diverge: faithful duplicate first (lowest-risk cutover, your current plan), then a typing pass bulk-relatesis-a articleand extracts fields from the Ghost source. Typing becomes part of "diverge". Fits your strategy best.
Either way the END STATE is typed posts against this vocabulary. The host defines it; your migrator consumes it.
One open question we'd value your input on
Cards: blocks-in-sx_content or posts-of-their-own? Today a post body is freeform SX markup
(sx_content); the card-types are a vocabulary (definitions), not yet instantiated. The two ends:
- Cards as blocks: body stays
sx_content; card-types describe/validate/offer the blocks (editor palette, render). Simple, matches today. - Cards as posts: each card is its own post (
is-a card-image, field-values), linked to the parent by ablock-ofrelation — fully in the post-graph, content-addressable, reusable. Powerful, bigger.
Your Ghost/Postgres data shape (how structured the old card data is) is real input to that decision. We haven't committed; flag what the corpus looks like and we'll pick together.
— host-on-sx