host: typed Ghost import — POST /import lands old posts as first-class Articles
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 1m0s

The genesis-import seam for the loops/radar migration (NOTE-blog-types-for-radar.md):
an old Ghost post lands not as bare sx_content but as a TYPED Article.

- host/blog-import-post!(ghost-dict): put! the {slug,title,sx_content,status} record +
  is-a article + Ghost columns -> article :field-values (custom_excerpt->subtitle,
  feature_image->hero) + tags -> tag-posts with tagged edges. Idempotent. The Ghost body
  is already sx_content ((~kg_cards/kg-*) from the Python lexical_to_sx migration), so we
  carry it as-is. host/blog-import-all! for batches.
- POST /import (guarded): body = a text/sx LIST of Ghost column dicts (radar's Postgres
  reader serialises rows to this); imports each typed; -> {:ok true :data {:imported N
  :slugs (...)}}. Runs in the serving handler (IO resolver installed) so the per-post/
  per-tag loops are JIT-safe.

Verified live-path end-to-end (ephemeral SX_SERVING_JIT=1): POST a fixture Ghost post ->
imported 1; the post's edit form is pre-filled (subtitle='An imported standfirst',
hero=the feature image), its page renders the subtitle standfirst via the article template
+ the body, and its tags (News/SX) land in the graph. Tests added; full blog suite still
blocked by box contention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 15:05:02 +00:00
parent 8f8688805e
commit fac15d6140
2 changed files with 65 additions and 1 deletions

View File

@@ -716,6 +716,27 @@
(let ((body (dream-resp-body (host-bl-app (host-bl-req "/meta")))))
(list (contains? body ">Image</a>") (contains? body "src:URL, alt:String")))
(list true true))
;; -- typed Ghost import (the radar genesis-import seam) --
(host-bl-test "import-post! lands a Ghost post as a typed Article + fields + tags"
(begin
(host/blog-import-post! {"slug" "g1" "title" "G1" "sx_content" "(article (h1 \"G1\"))"
"status" "published" "custom_excerpt" "A standfirst"
"feature_image" "http://i/h.jpg" "tags" (list "News")})
(list (host/blog-is-a? "g1" "article")
(get (host/blog-field-values-of "g1") "subtitle")
(get (host/blog-field-values-of "g1") "hero")
(contains? (host/blog-out "g1" "tagged") "news")))
(list true "A standfirst" "http://i/h.jpg" true))
(host-bl-test "POST /import (text/sx list of Ghost dicts) lands typed posts"
(begin
(host-bl-wapp (host-bl-send "POST" "/import" "Bearer good" "text/sx"
"({:slug \"g2\" :title \"G2\" :sx_content \"(p \\\"b\\\")\" :status \"published\" :custom_excerpt \"S2\"})"))
(list (host/blog-is-a? "g2" "article") (get (host/blog-field-values-of "g2") "subtitle")))
(list true "S2"))
(host-bl-test "POST /import rejects a non-list body -> 400"
(dream-status (host-bl-wapp (host-bl-send "POST" "/import" "Bearer good" "text/sx" "{:x 1}")))
400)
(host-bl-test "a post with no schema'd type is vacuously valid"
(host/blog-type-valid? "ppost" "(p \"anything\")") true)
(host-bl-test "edit-submit rejects content violating the type schema (not saved)"