Lacuna/Labs
cross-post-bulk-to-google
pink
#199 / 601

NAME

cross-post-bulk-to-google — Operator selects N listings, Sakura adapts and pushes the batch. Per-listing failures don't abort the batch.

SYNOPSIS

id cross-post-bulk-to-google tier pink section §7. Pink — Sakura local (65 baseline, 131 post-matrix) trigger operator-commanded tools merchant_center/sync source new

DESCRIPTION

Sakura's local 8B model handles this on-device. Free, private, Cortex-grounded. The 'I just know what you have' tier.

Operator selects N listings, Sakura adapts and pushes the batch. Per-listing failures don't abort the batch.

MECHANISM

PREfetchGUARDtier: pinkACTperformRESULTrenderON-ERRretry / esc
;;~ title    "Cross-post · bulk to Google Shopping"
;;~ author   "lacuna"
;;~ version  1
;;~ mode     automation
;;~ flavor   purple
;;~ id       cross-post-bulk-to-google
;;~ touches  (etsy-listings ebay-listings shopify-listings google-merchant)
;;~ summary  "Operator-commanded. Operator selects N listings; cart adapts and pushes each in turn with rate-limit-aware pacing. Per-listing failures don't abort the batch — they're collected and surfaced at the end. Tier-gated to Magic+; drafts never sync."

;; Bulk cross-post. The operator selects a batch (in the listing grid
;; or via "all active") and confirms; the cart walks the batch, pushing
;; each listing in turn.
;;
;; Recursion shape: pop head off the input list, push, branch on result;
;; on success drop the head and recurse; on per-listing failure record
;; the failure and continue. The batch is allowed to partial-succeed —
;; the operator gets a final report of which landed and which didn't.

(cart 'cross-post-bulk-to-google
  '((author    . "lacuna")
    (version   . 1)
    (touches   . (etsy-listings ebay-listings shopify-listings google-merchant))
    (read-only . #f)))

;; ── start ──────────────────────────────────────────────────────
;; ctx carries 'listings — a list of listing records (already fetched
;; by the operator surface; the cart doesn't fetch them itself). Empty
;; list = nothing to do; escalate calmly.
(define (start ctx)
  (let ((listings (ctx-get 'listings ctx)))
    (cond
      ((null? listings) (escalate 'empty-batch null))
      (else (next 'check-merchant
                  (ctx-set 'failures '()
                  (ctx-set 'successes '() ctx)))))))

;; ── check-merchant ─────────────────────────────────────────────
;; Verify the operator has the merchant center connected before we
;; start the batch — failing fast here saves N round-trips.
(define (check-merchant ctx)
  (act 'merchant_center.status '() 'check-merchant-result))

(define (check-merchant-result ctx)
  (let ((status (ctx-result ctx)))
    (cond
      ((not (status-connected? status))
       (escalate 'merchant-not-connected null))
      ((status-pending-verification? status)
       (after 3600 'check-merchant ctx))
      (else (next 'pop-and-push ctx)))))

;; ── pop-and-push — the loop head ──────────────────────────────
;; Peel one listing off the head, push it, branch in a named result
;; fn. Drafts in the batch are silently skipped (the operator surface
;; usually filters drafts out, but the cart's contract is "drafts
;; never sync" so we belt-and-suspender it).
(define (pop-and-push ctx)
  (let ((listings (ctx-get 'listings ctx)))
    (cond
      ((null? listings) (next 'finalize ctx))
      ((listing-is-draft? (car listings))
       (next 'pop-and-push
             (ctx-set 'listings (cdr listings) ctx)))
      (else
       (act 'merchant_center/sync (list (car listings))
            'check-batch-result)))))

;; ── check-batch-result ────────────────────────────────────────
;; Record the outcome, drop the head, recurse. Per-listing failures
;; are collected into 'failures so the operator can see what broke
;; at the end. We pace at 30s on rate-limit; otherwise hard-step.
(define (check-batch-result ctx)
  (let* ((r        (ctx-result ctx))
         (listings (ctx-get 'listings ctx))
         (head     (car listings))
         (rest     (cdr listings)))
    (cond
      ((push-ok? r)
       (next 'pop-and-push
             (ctx-set 'listings rest
             (ctx-set 'successes
                      (cons (push-product-id r) (ctx-get 'successes ctx))
                      ctx))))
      ((eq? r 'rate-limited)
       ;; Keep the head — don't drop it; we retry after the backoff.
       (after 30 'pop-and-push ctx))
      (else
       (next 'pop-and-push
             (ctx-set 'listings rest
             (ctx-set 'failures
                      (cons (list (listing-id head) r) (ctx-get 'failures ctx))
                      ctx)))))))

;; ── finalize — operator-facing summary ────────────────────────
(define (finalize ctx)
  (let ((wins  (length (ctx-get 'successes ctx)))
        (fails (length (ctx-get 'failures  ctx))))
    (sakura/say (string-append
                  "Bulk cross-post complete · "
                  (number->string wins) " ok"
                  (if (> fails 0)
                      (string-append " · " (number->string fails) " flagged")
                      "")))
    (done)))

source: curator-web/src/scheme/carts/google/cross-post-bulk-to-google.sks · 104 lines

EXAMPLES

Example forthcoming — docs/automations/examples.json entry editable in place.

TIER

pink
Sakura local 8B · free · on-device

FITNESS

Ready

Fits any shop today — this one has no sample-size floor.

SEE ALSO

HISTORY

canonical source: curator/docs/AUTOMATIONS-CANONICAL.md · §7-pink-sakura-local-65-baseline-131-post-matrix
last regenerated: 2026-06-05 20:31 UTC (mechanical — edits land in the MD or sidecars, not here)

KNOWN BUGS

(none recorded — flag in AUTOMATIONS-CANONICAL.md or open an issue against the cart file)