← back to Cli Printing Press
fix(cli): route sync --json events to stdout (#608)
6281f2be60295c0f5fd9e3984b7c5ca169da6dd5 · 2026-05-05 00:47:14 -0700 · Trevin Chow
* fix(cli): route sync --json events to stdout
Sync template's 21 JSON-event-emit lines (sync_start, sync_summary,
sync_warning, sync_error, sync_anomaly, sync_progress, sync_complete)
flip from os.Stderr to os.Stdout. Under --json, every emitted line is
now valid JSON on stdout, so <cli> sync --json | jq works as the
canonical agent flow.
Human-friendly progress (Sync complete: ..., warning: ..., per-resource
status text) stays on stderr unchanged. Sync event schemas, names, and
ordering are unchanged. The existing humanFriendly conditional already
gated plain-text variants to stderr; this fix is the symmetric routing
for the JSON variants.
Reconciles with the ElevenLabs convention from
docs/plans/2026-03-25-feat-elevenlabs-learnings-human-friendly-ndjson-plan.md:
that plan prohibits text events corrupting a JSON array on stdout.
NDJSON-on-stdout (every line a parseable JSON object) satisfies the
contract — the ban is on plain-text pollution, not on structured events.
Issue #592 enumerated 19 lines; the template has since gained 2 more
(parent-resource sync events at lines 1077, 1084). This PR's grep-based
swap catches all 21 by anchoring on the literal `{"event":` raw-string
prefix, so future event additions inherit stdout routing automatically.
The two affected goldens (generate-golden-api,
generate-tier-routing-api) re-baseline in the next commit.
Closes #592.
Plan: docs/plans/2026-05-04-005-fix-wave-1-retro-blockers-plan.md (U4)
* fix(cli): sort resource keys for deterministic schema generation
BuildSchema in internal/generator/schema_builder.go iterated s.Resources
and resource.SubResources directly — both maps with randomized iteration
order. The resulting []TableDef inherited the random order, causing
generated `switch resource { case "..." }` blocks in printed CLIs to
shuffle between runs. Local generations and CI generations could disagree
on ordering, which surfaced as a golden mismatch on PR-608 even though
the source was identical (golden verify diff showed `case "summary"`
moving from the bottom of the switch to the top).
Fix: collect resource keys, sort.Strings them, then iterate in sorted
order. Sub-resources within a parent get the same treatment. The
deduplication step downstream preserves first-seen order, which is now
deterministic.
This is a latent generator bug surfaced (not caused) by PR-608's golden
re-baseline. Without this fix, any change touching sync.go.tmpl or the
schema would have hit the same flake.
Plan: docs/plans/2026-05-04-005-fix-wave-1-retro-blockers-plan.md (U4)
* test(cli): re-baseline sync.go goldens for stdout routing
Two generated-source goldens (generate-golden-api,
generate-tier-routing-api) include a printed CLI's internal/cli/sync.go.
Each picks up exactly the same os.Stderr → os.Stdout swap on every
{\"event\":...} JSON-emit line; human-friendly stderr lines unchanged.
The previous re-baseline (in the now-rewritten branch history) captured
non-deterministic switch-case order from BuildSchema's randomized map
iteration. With the schema sort fix in the prior commit, regeneration
is reproducible — verified across three consecutive scripts/golden.sh
verify runs (11/11 pass each).
The dogfood-verdict-matrix golden case continues to pass without
fixture regeneration — it asserts on dogfood verdicts, not on sync's
stdout/stderr routing.
Plan: docs/plans/2026-05-04-005-fix-wave-1-retro-blockers-plan.md (U4)
Files touched
M internal/generator/schema_builder.goM internal/generator/templates/sync.go.tmplM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.goM testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go
Diff
commit 6281f2be60295c0f5fd9e3984b7c5ca169da6dd5
Author: Trevin Chow <trevin@trevinchow.com>
Date: Tue May 5 00:47:14 2026 -0700
fix(cli): route sync --json events to stdout (#608)
* fix(cli): route sync --json events to stdout
Sync template's 21 JSON-event-emit lines (sync_start, sync_summary,
sync_warning, sync_error, sync_anomaly, sync_progress, sync_complete)
flip from os.Stderr to os.Stdout. Under --json, every emitted line is
now valid JSON on stdout, so <cli> sync --json | jq works as the
canonical agent flow.
Human-friendly progress (Sync complete: ..., warning: ..., per-resource
status text) stays on stderr unchanged. Sync event schemas, names, and
ordering are unchanged. The existing humanFriendly conditional already
gated plain-text variants to stderr; this fix is the symmetric routing
for the JSON variants.
Reconciles with the ElevenLabs convention from
docs/plans/2026-03-25-feat-elevenlabs-learnings-human-friendly-ndjson-plan.md:
that plan prohibits text events corrupting a JSON array on stdout.
NDJSON-on-stdout (every line a parseable JSON object) satisfies the
contract — the ban is on plain-text pollution, not on structured events.
Issue #592 enumerated 19 lines; the template has since gained 2 more
(parent-resource sync events at lines 1077, 1084). This PR's grep-based
swap catches all 21 by anchoring on the literal `{"event":` raw-string
prefix, so future event additions inherit stdout routing automatically.
The two affected goldens (generate-golden-api,
generate-tier-routing-api) re-baseline in the next commit.
Closes #592.
Plan: docs/plans/2026-05-04-005-fix-wave-1-retro-blockers-plan.md (U4)
* fix(cli): sort resource keys for deterministic schema generation
BuildSchema in internal/generator/schema_builder.go iterated s.Resources
and resource.SubResources directly — both maps with randomized iteration
order. The resulting []TableDef inherited the random order, causing
generated `switch resource { case "..." }` blocks in printed CLIs to
shuffle between runs. Local generations and CI generations could disagree
on ordering, which surfaced as a golden mismatch on PR-608 even though
the source was identical (golden verify diff showed `case "summary"`
moving from the bottom of the switch to the top).
Fix: collect resource keys, sort.Strings them, then iterate in sorted
order. Sub-resources within a parent get the same treatment. The
deduplication step downstream preserves first-seen order, which is now
deterministic.
This is a latent generator bug surfaced (not caused) by PR-608's golden
re-baseline. Without this fix, any change touching sync.go.tmpl or the
schema would have hit the same flake.
Plan: docs/plans/2026-05-04-005-fix-wave-1-retro-blockers-plan.md (U4)
* test(cli): re-baseline sync.go goldens for stdout routing
Two generated-source goldens (generate-golden-api,
generate-tier-routing-api) include a printed CLI's internal/cli/sync.go.
Each picks up exactly the same os.Stderr → os.Stdout swap on every
{\"event\":...} JSON-emit line; human-friendly stderr lines unchanged.
The previous re-baseline (in the now-rewritten branch history) captured
non-deterministic switch-case order from BuildSchema's randomized map
iteration. With the schema sort fix in the prior commit, regeneration
is reproducible — verified across three consecutive scripts/golden.sh
verify runs (11/11 pass each).
The dogfood-verdict-matrix golden case continues to pass without
fixture regeneration — it asserts on dogfood verdicts, not on sync's
stdout/stderr routing.
Plan: docs/plans/2026-05-04-005-fix-wave-1-retro-blockers-plan.md (U4)
---
internal/generator/schema_builder.go | 24 +++++++++++--
internal/generator/templates/sync.go.tmpl | 42 +++++++++++-----------
.../printing-press-golden/internal/cli/sync.go | 42 +++++++++++-----------
.../tier-routing-golden/internal/cli/sync.go | 30 ++++++++--------
4 files changed, 79 insertions(+), 59 deletions(-)
diff --git a/internal/generator/schema_builder.go b/internal/generator/schema_builder.go
index b845b471..93919146 100644
--- a/internal/generator/schema_builder.go
+++ b/internal/generator/schema_builder.go
@@ -1,6 +1,7 @@
package generator
import (
+ "sort"
"strings"
"unicode"
@@ -36,7 +37,18 @@ type IndexDef struct {
func BuildSchema(s *spec.APISpec) []TableDef {
var tables []TableDef
- for name, resource := range s.Resources {
+ // Iterate resources in sorted order so generated output is deterministic
+ // across runs and platforms — Go map iteration is randomized, and the
+ // emitted switch statements in sync.go would otherwise drift between
+ // generations and break golden verification.
+ resourceNames := make([]string, 0, len(s.Resources))
+ for name := range s.Resources {
+ resourceNames = append(resourceNames, name)
+ }
+ sort.Strings(resourceNames)
+
+ for _, name := range resourceNames {
+ resource := s.Resources[name]
gravity := computeDataGravity(name, resource)
tableName := toSnakeCase(name)
@@ -90,7 +102,15 @@ func BuildSchema(s *spec.APISpec) []TableDef {
tables = append(tables, table)
- for subName, subResource := range resource.SubResources {
+ // Same determinism concern as the outer loop: sub-resources from the
+ // same parent need a stable ordering in generated output.
+ subNames := make([]string, 0, len(resource.SubResources))
+ for sn := range resource.SubResources {
+ subNames = append(subNames, sn)
+ }
+ sort.Strings(subNames)
+ for _, subName := range subNames {
+ subResource := resource.SubResources[subName]
subTable := buildSubResourceTable(subName, subResource, tableName)
tables = append(tables, subTable)
}
diff --git a/internal/generator/templates/sync.go.tmpl b/internal/generator/templates/sync.go.tmpl
index 73f837c1..2b9ff84e 100644
--- a/internal/generator/templates/sync.go.tmpl
+++ b/internal/generator/templates/sync.go.tmpl
@@ -251,7 +251,7 @@ Exit codes & warnings:
totalSynced, totalResources, elapsed.Seconds())
}
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_summary","total_records":%d,"resources":%d,"success":%d,"warned":%d,"errored":%d,"duration_ms":%d}`+"\n",
+ fmt.Fprintf(os.Stdout, `{"event":"sync_summary","total_records":%d,"resources":%d,"success":%d,"warned":%d,"errored":%d,"duration_ms":%d}`+"\n",
totalSynced, totalResources, successCount, warnCount, errCount, elapsed.Milliseconds())
}
@@ -281,7 +281,7 @@ Exit codes & warnings:
if errCount > 0 && !strict && criticalErrCount == 0 && successCount > 0 {
if !humanFriendly {
msg := fmt.Sprintf("%d resource(s) failed but exit code is 0 because the new default treats non-critical failures as warnings. Pass --strict to restore the old behavior, or annotate critical resources with x-critical: true. See CHANGELOG.", errCount)
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","reason":"exit_policy_default_changed","errored":%d,"message":"%s"}`+"\n",
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","reason":"exit_policy_default_changed","errored":%d,"message":"%s"}`+"\n",
errCount, strings.ReplaceAll(msg, `"`, `\"`))
} else {
fmt.Fprintf(os.Stderr, "warning: %d resource(s) failed but exit code is 0 because the new default treats non-critical failures as warnings. Pass --strict to restore the old behavior, or annotate critical resources with x-critical: true.\n", errCount)
@@ -315,7 +315,7 @@ func syncResource(c interface {
started := time.Now()
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_start","resource":"%s"}`+"\n", resource)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_start","resource":"%s"}`+"\n", resource)
}
path, err := syncResourcePath(resource)
@@ -381,13 +381,13 @@ func syncResource(c interface {
if err != nil {
if w, ok := isSyncAccessWarning(err); ok {
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","status":%d,"reason":"%s","message":"%s"}`+"\n",
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","status":%d,"reason":"%s","message":"%s"}`+"\n",
resource, w.Status, w.Reason, strings.ReplaceAll(w.Message, `"`, `\"`))
}
return syncResult{Resource: resource, Count: totalCount, Warn: fmt.Errorf("skipped %s: %s", resource, w.Reason), Duration: time.Since(started)}
}
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
}
return syncResult{Resource: resource, Count: totalCount, Err: fmt.Errorf("fetching %s: %w", resource, err), Duration: time.Since(started)}
}
@@ -400,7 +400,7 @@ func syncResource(c interface {
// Single object response - try to store as-is
if err := upsertSingleObject(db, resource, data); err != nil {
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
}
return syncResult{Resource: resource, Err: err, Duration: time.Since(started)}
}
@@ -422,7 +422,7 @@ func syncResource(c interface {
stored, extractFailures, err := upsertResourceBatch(db, resource, items)
if err != nil {
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
}
return syncResult{Resource: resource, Count: totalCount, Err: fmt.Errorf("upserting batch for %s: %w", resource, err), Duration: time.Since(started)}
}
@@ -439,7 +439,7 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "warning: %s returned %d items but stored 0 — the local store will be empty for this resource. Likely cause: scalar item shape rather than objects with extractable IDs.\n", resource, len(items))
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"reason":"all_items_failed_id_extraction"}`+"\n", resource, len(items))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"reason":"all_items_failed_id_extraction"}`+"\n", resource, len(items))
}
anomalyEmitted = true
} else if extractFailures > 0 && !anomalyEmitted {
@@ -450,7 +450,7 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\nwarning: %s had %d item(s) on this page with no extractable primary key — those rows were dropped silently. Annotate the spec with x-resource-id to fix.\n", resource, extractFailures)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":%d,"count":%d,"reason":"primary_key_unresolved"}`+"\n", resource, len(items), stored, extractFailures)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":%d,"count":%d,"reason":"primary_key_unresolved"}`+"\n", resource, len(items), stored, extractFailures)
}
anomalyEmitted = true
}
@@ -468,9 +468,9 @@ func syncResource(c interface {
}
} else {
if currentRate > 0 {
- fmt.Fprintf(os.Stderr, `{"event":"sync_progress","resource":"%s","fetched":%d,"rate_rps":%.1f}`+"\n", resource, atomic.LoadInt64(&progressCount), currentRate)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_progress","resource":"%s","fetched":%d,"rate_rps":%.1f}`+"\n", resource, atomic.LoadInt64(&progressCount), currentRate)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_progress","resource":"%s","fetched":%d}`+"\n", resource, atomic.LoadInt64(&progressCount))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_progress","resource":"%s","fetched":%d}`+"\n", resource, atomic.LoadInt64(&progressCount))
}
}
@@ -487,7 +487,7 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\n %s: reached --max-pages limit (%d pages, %d items)\n", resource, maxPages, totalCount)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","reason":"max_pages_cap_hit","message":"reached --max-pages cap of %d; data may be truncated. Re-run with --max-pages 0 (unlimited) or higher to verify."}`+"\n", resource, maxPages)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","reason":"max_pages_cap_hit","message":"reached --max-pages cap of %d; data may be truncated. Re-run with --max-pages 0 (unlimited) or higher to verify."}`+"\n", resource, maxPages)
}
break
}
@@ -502,7 +502,7 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\n %s: API returned the same next cursor across two pages; aborting to prevent budget waste.\n", resource)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","reason":"stuck_pagination","message":"API returned the same next cursor across two pages for resource %s; aborting to prevent budget waste."}`+"\n", resource, resource)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","reason":"stuck_pagination","message":"API returned the same next cursor across two pages for resource %s; aborting to prevent budget waste."}`+"\n", resource, resource)
}
break
}
@@ -530,12 +530,12 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\nwarning: %s consumed %d items, extracted %d primary keys, but stored 0 rows — extraction succeeded yet nothing landed. Investigate FTS triggers / transaction rollback / encoding.\n", resource, consumedTotal, consumedTotal-extractFailureTotal)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"extract_failures":%d,"reason":"stored_count_zero_after_extraction"}`+"\n", resource, consumedTotal, extractFailureTotal)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"extract_failures":%d,"reason":"stored_count_zero_after_extraction"}`+"\n", resource, consumedTotal, extractFailureTotal)
}
}
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_complete","resource":"%s","total":%d,"duration_ms":%d}`+"\n", resource, totalCount, time.Since(started).Milliseconds())
+ fmt.Fprintf(os.Stdout, `{"event":"sync_complete","resource":"%s","total":%d,"duration_ms":%d}`+"\n", resource, totalCount, time.Since(started).Milliseconds())
}
return syncResult{Resource: resource, Count: totalCount, Duration: time.Since(started)}
@@ -1030,7 +1030,7 @@ func syncDependentResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\n %s: access denied for parent %s: %s\n", dep.Name, parentID, w.Reason)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","parent":"%s","status":%d,"reason":"%s","message":"%s"}`+"\n",
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","parent":"%s","status":%d,"reason":"%s","message":"%s"}`+"\n",
dep.Name, parentID, w.Status, w.Reason, strings.ReplaceAll(w.Message, `"`, `\"`))
}
} else if humanFriendly {
@@ -1074,14 +1074,14 @@ func syncDependentResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\nwarning: %s returned %d items for parent %s but stored 0 — likely scalar item shape or unrecognized primary-key field name.\n", dep.Name, len(items), parentID)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","parent":"%s","consumed":%d,"stored":0,"reason":"all_items_failed_id_extraction"}`+"\n", dep.Name, parentID, len(items))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","parent":"%s","consumed":%d,"stored":0,"reason":"all_items_failed_id_extraction"}`+"\n", dep.Name, parentID, len(items))
}
depAnomalyEmitted = true
} else if extractFailures > 0 && stored > 0 && !depAnomalyEmitted {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\nwarning: %s had %d item(s) with no extractable primary key — those rows were dropped silently. Annotate the spec with x-resource-id to fix.\n", dep.Name, extractFailures)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","parent":"%s","consumed":%d,"stored":%d,"count":%d,"reason":"primary_key_unresolved"}`+"\n", dep.Name, parentID, len(items), stored, extractFailures)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","parent":"%s","consumed":%d,"stored":%d,"count":%d,"reason":"primary_key_unresolved"}`+"\n", dep.Name, parentID, len(items), stored, extractFailures)
}
depAnomalyEmitted = true
}
@@ -1093,7 +1093,7 @@ func syncDependentResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\n %s: reached --max-pages limit (%d pages, %d items) for parent %s\n", dep.Name, maxPages, totalCount, parentID)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","parent":"%s","reason":"max_pages_cap_hit","message":"reached --max-pages cap of %d; data may be truncated. Re-run with --max-pages 0 (unlimited) or higher to verify."}`+"\n", dep.Name, parentID, maxPages)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","parent":"%s","reason":"max_pages_cap_hit","message":"reached --max-pages cap of %d; data may be truncated. Re-run with --max-pages 0 (unlimited) or higher to verify."}`+"\n", dep.Name, parentID, maxPages)
}
break
}
@@ -1104,7 +1104,7 @@ func syncDependentResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\n %s: API returned the same next cursor across two pages for parent %s; aborting to prevent budget waste.\n", dep.Name, parentID)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","parent":"%s","reason":"stuck_pagination","message":"API returned the same next cursor across two pages for resource %s; aborting to prevent budget waste."}`+"\n", dep.Name, parentID, dep.Name)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","parent":"%s","reason":"stuck_pagination","message":"API returned the same next cursor across two pages for resource %s; aborting to prevent budget waste."}`+"\n", dep.Name, parentID, dep.Name)
}
break
}
@@ -1131,7 +1131,7 @@ func syncDependentResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\nwarning: %s consumed %d items, extracted %d primary keys, but stored 0 rows — extraction succeeded yet nothing landed. Investigate FTS triggers / transaction rollback / encoding.\n", dep.Name, depConsumedTotal, depConsumedTotal-depExtractFailureTotal)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"extract_failures":%d,"reason":"stored_count_zero_after_extraction"}`+"\n", dep.Name, depConsumedTotal, depExtractFailureTotal)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"extract_failures":%d,"reason":"stored_count_zero_after_extraction"}`+"\n", dep.Name, depConsumedTotal, depExtractFailureTotal)
}
}
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
index 50694e61..6394ea13 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/sync.go
@@ -232,7 +232,7 @@ Exit codes & warnings:
totalSynced, totalResources, elapsed.Seconds())
}
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_summary","total_records":%d,"resources":%d,"success":%d,"warned":%d,"errored":%d,"duration_ms":%d}`+"\n",
+ fmt.Fprintf(os.Stdout, `{"event":"sync_summary","total_records":%d,"resources":%d,"success":%d,"warned":%d,"errored":%d,"duration_ms":%d}`+"\n",
totalSynced, totalResources, successCount, warnCount, errCount, elapsed.Milliseconds())
}
@@ -262,7 +262,7 @@ Exit codes & warnings:
if errCount > 0 && !strict && criticalErrCount == 0 && successCount > 0 {
if !humanFriendly {
msg := fmt.Sprintf("%d resource(s) failed but exit code is 0 because the new default treats non-critical failures as warnings. Pass --strict to restore the old behavior, or annotate critical resources with x-critical: true. See CHANGELOG.", errCount)
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","reason":"exit_policy_default_changed","errored":%d,"message":"%s"}`+"\n",
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","reason":"exit_policy_default_changed","errored":%d,"message":"%s"}`+"\n",
errCount, strings.ReplaceAll(msg, `"`, `\"`))
} else {
fmt.Fprintf(os.Stderr, "warning: %d resource(s) failed but exit code is 0 because the new default treats non-critical failures as warnings. Pass --strict to restore the old behavior, or annotate critical resources with x-critical: true.\n", errCount)
@@ -293,7 +293,7 @@ func syncResource(c interface {
started := time.Now()
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_start","resource":"%s"}`+"\n", resource)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_start","resource":"%s"}`+"\n", resource)
}
path, err := syncResourcePath(resource)
@@ -352,13 +352,13 @@ func syncResource(c interface {
if err != nil {
if w, ok := isSyncAccessWarning(err); ok {
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","status":%d,"reason":"%s","message":"%s"}`+"\n",
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","status":%d,"reason":"%s","message":"%s"}`+"\n",
resource, w.Status, w.Reason, strings.ReplaceAll(w.Message, `"`, `\"`))
}
return syncResult{Resource: resource, Count: totalCount, Warn: fmt.Errorf("skipped %s: %s", resource, w.Reason), Duration: time.Since(started)}
}
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
}
return syncResult{Resource: resource, Count: totalCount, Err: fmt.Errorf("fetching %s: %w", resource, err), Duration: time.Since(started)}
}
@@ -371,7 +371,7 @@ func syncResource(c interface {
// Single object response - try to store as-is
if err := upsertSingleObject(db, resource, data); err != nil {
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
}
return syncResult{Resource: resource, Err: err, Duration: time.Since(started)}
}
@@ -393,7 +393,7 @@ func syncResource(c interface {
stored, extractFailures, err := upsertResourceBatch(db, resource, items)
if err != nil {
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
}
return syncResult{Resource: resource, Count: totalCount, Err: fmt.Errorf("upserting batch for %s: %w", resource, err), Duration: time.Since(started)}
}
@@ -410,7 +410,7 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "warning: %s returned %d items but stored 0 — the local store will be empty for this resource. Likely cause: scalar item shape rather than objects with extractable IDs.\n", resource, len(items))
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"reason":"all_items_failed_id_extraction"}`+"\n", resource, len(items))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"reason":"all_items_failed_id_extraction"}`+"\n", resource, len(items))
}
anomalyEmitted = true
} else if extractFailures > 0 && !anomalyEmitted {
@@ -421,7 +421,7 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\nwarning: %s had %d item(s) on this page with no extractable primary key — those rows were dropped silently. Annotate the spec with x-resource-id to fix.\n", resource, extractFailures)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":%d,"count":%d,"reason":"primary_key_unresolved"}`+"\n", resource, len(items), stored, extractFailures)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":%d,"count":%d,"reason":"primary_key_unresolved"}`+"\n", resource, len(items), stored, extractFailures)
}
anomalyEmitted = true
}
@@ -439,9 +439,9 @@ func syncResource(c interface {
}
} else {
if currentRate > 0 {
- fmt.Fprintf(os.Stderr, `{"event":"sync_progress","resource":"%s","fetched":%d,"rate_rps":%.1f}`+"\n", resource, atomic.LoadInt64(&progressCount), currentRate)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_progress","resource":"%s","fetched":%d,"rate_rps":%.1f}`+"\n", resource, atomic.LoadInt64(&progressCount), currentRate)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_progress","resource":"%s","fetched":%d}`+"\n", resource, atomic.LoadInt64(&progressCount))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_progress","resource":"%s","fetched":%d}`+"\n", resource, atomic.LoadInt64(&progressCount))
}
}
@@ -458,7 +458,7 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\n %s: reached --max-pages limit (%d pages, %d items)\n", resource, maxPages, totalCount)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","reason":"max_pages_cap_hit","message":"reached --max-pages cap of %d; data may be truncated. Re-run with --max-pages 0 (unlimited) or higher to verify."}`+"\n", resource, maxPages)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","reason":"max_pages_cap_hit","message":"reached --max-pages cap of %d; data may be truncated. Re-run with --max-pages 0 (unlimited) or higher to verify."}`+"\n", resource, maxPages)
}
break
}
@@ -473,7 +473,7 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\n %s: API returned the same next cursor across two pages; aborting to prevent budget waste.\n", resource)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","reason":"stuck_pagination","message":"API returned the same next cursor across two pages for resource %s; aborting to prevent budget waste."}`+"\n", resource, resource)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","reason":"stuck_pagination","message":"API returned the same next cursor across two pages for resource %s; aborting to prevent budget waste."}`+"\n", resource, resource)
}
break
}
@@ -501,12 +501,12 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\nwarning: %s consumed %d items, extracted %d primary keys, but stored 0 rows — extraction succeeded yet nothing landed. Investigate FTS triggers / transaction rollback / encoding.\n", resource, consumedTotal, consumedTotal-extractFailureTotal)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"extract_failures":%d,"reason":"stored_count_zero_after_extraction"}`+"\n", resource, consumedTotal, extractFailureTotal)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"extract_failures":%d,"reason":"stored_count_zero_after_extraction"}`+"\n", resource, consumedTotal, extractFailureTotal)
}
}
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_complete","resource":"%s","total":%d,"duration_ms":%d}`+"\n", resource, totalCount, time.Since(started).Milliseconds())
+ fmt.Fprintf(os.Stdout, `{"event":"sync_complete","resource":"%s","total":%d,"duration_ms":%d}`+"\n", resource, totalCount, time.Since(started).Milliseconds())
}
return syncResult{Resource: resource, Count: totalCount, Duration: time.Since(started)}
@@ -935,7 +935,7 @@ func syncDependentResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\n %s: access denied for parent %s: %s\n", dep.Name, parentID, w.Reason)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","parent":"%s","status":%d,"reason":"%s","message":"%s"}`+"\n",
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","parent":"%s","status":%d,"reason":"%s","message":"%s"}`+"\n",
dep.Name, parentID, w.Status, w.Reason, strings.ReplaceAll(w.Message, `"`, `\"`))
}
} else if humanFriendly {
@@ -979,14 +979,14 @@ func syncDependentResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\nwarning: %s returned %d items for parent %s but stored 0 — likely scalar item shape or unrecognized primary-key field name.\n", dep.Name, len(items), parentID)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","parent":"%s","consumed":%d,"stored":0,"reason":"all_items_failed_id_extraction"}`+"\n", dep.Name, parentID, len(items))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","parent":"%s","consumed":%d,"stored":0,"reason":"all_items_failed_id_extraction"}`+"\n", dep.Name, parentID, len(items))
}
depAnomalyEmitted = true
} else if extractFailures > 0 && stored > 0 && !depAnomalyEmitted {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\nwarning: %s had %d item(s) with no extractable primary key — those rows were dropped silently. Annotate the spec with x-resource-id to fix.\n", dep.Name, extractFailures)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","parent":"%s","consumed":%d,"stored":%d,"count":%d,"reason":"primary_key_unresolved"}`+"\n", dep.Name, parentID, len(items), stored, extractFailures)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","parent":"%s","consumed":%d,"stored":%d,"count":%d,"reason":"primary_key_unresolved"}`+"\n", dep.Name, parentID, len(items), stored, extractFailures)
}
depAnomalyEmitted = true
}
@@ -998,7 +998,7 @@ func syncDependentResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\n %s: reached --max-pages limit (%d pages, %d items) for parent %s\n", dep.Name, maxPages, totalCount, parentID)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","parent":"%s","reason":"max_pages_cap_hit","message":"reached --max-pages cap of %d; data may be truncated. Re-run with --max-pages 0 (unlimited) or higher to verify."}`+"\n", dep.Name, parentID, maxPages)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","parent":"%s","reason":"max_pages_cap_hit","message":"reached --max-pages cap of %d; data may be truncated. Re-run with --max-pages 0 (unlimited) or higher to verify."}`+"\n", dep.Name, parentID, maxPages)
}
break
}
@@ -1009,7 +1009,7 @@ func syncDependentResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\n %s: API returned the same next cursor across two pages for parent %s; aborting to prevent budget waste.\n", dep.Name, parentID)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","parent":"%s","reason":"stuck_pagination","message":"API returned the same next cursor across two pages for resource %s; aborting to prevent budget waste."}`+"\n", dep.Name, parentID, dep.Name)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","parent":"%s","reason":"stuck_pagination","message":"API returned the same next cursor across two pages for resource %s; aborting to prevent budget waste."}`+"\n", dep.Name, parentID, dep.Name)
}
break
}
@@ -1036,7 +1036,7 @@ func syncDependentResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\nwarning: %s consumed %d items, extracted %d primary keys, but stored 0 rows — extraction succeeded yet nothing landed. Investigate FTS triggers / transaction rollback / encoding.\n", dep.Name, depConsumedTotal, depConsumedTotal-depExtractFailureTotal)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"extract_failures":%d,"reason":"stored_count_zero_after_extraction"}`+"\n", dep.Name, depConsumedTotal, depExtractFailureTotal)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"extract_failures":%d,"reason":"stored_count_zero_after_extraction"}`+"\n", dep.Name, depConsumedTotal, depExtractFailureTotal)
}
}
diff --git a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go
index 1c283e9a..935905a8 100644
--- a/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go
+++ b/testdata/golden/expected/generate-tier-routing-api/tier-routing-golden/internal/cli/sync.go
@@ -209,7 +209,7 @@ Exit codes & warnings:
totalSynced, totalResources, elapsed.Seconds())
}
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_summary","total_records":%d,"resources":%d,"success":%d,"warned":%d,"errored":%d,"duration_ms":%d}`+"\n",
+ fmt.Fprintf(os.Stdout, `{"event":"sync_summary","total_records":%d,"resources":%d,"success":%d,"warned":%d,"errored":%d,"duration_ms":%d}`+"\n",
totalSynced, totalResources, successCount, warnCount, errCount, elapsed.Milliseconds())
}
@@ -239,7 +239,7 @@ Exit codes & warnings:
if errCount > 0 && !strict && criticalErrCount == 0 && successCount > 0 {
if !humanFriendly {
msg := fmt.Sprintf("%d resource(s) failed but exit code is 0 because the new default treats non-critical failures as warnings. Pass --strict to restore the old behavior, or annotate critical resources with x-critical: true. See CHANGELOG.", errCount)
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","reason":"exit_policy_default_changed","errored":%d,"message":"%s"}`+"\n",
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","reason":"exit_policy_default_changed","errored":%d,"message":"%s"}`+"\n",
errCount, strings.ReplaceAll(msg, `"`, `\"`))
} else {
fmt.Fprintf(os.Stderr, "warning: %d resource(s) failed but exit code is 0 because the new default treats non-critical failures as warnings. Pass --strict to restore the old behavior, or annotate critical resources with x-critical: true.\n", errCount)
@@ -270,7 +270,7 @@ func syncResource(c interface {
started := time.Now()
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_start","resource":"%s"}`+"\n", resource)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_start","resource":"%s"}`+"\n", resource)
}
path, err := syncResourcePath(resource)
@@ -329,13 +329,13 @@ func syncResource(c interface {
if err != nil {
if w, ok := isSyncAccessWarning(err); ok {
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","status":%d,"reason":"%s","message":"%s"}`+"\n",
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","status":%d,"reason":"%s","message":"%s"}`+"\n",
resource, w.Status, w.Reason, strings.ReplaceAll(w.Message, `"`, `\"`))
}
return syncResult{Resource: resource, Count: totalCount, Warn: fmt.Errorf("skipped %s: %s", resource, w.Reason), Duration: time.Since(started)}
}
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
}
return syncResult{Resource: resource, Count: totalCount, Err: fmt.Errorf("fetching %s: %w", resource, err), Duration: time.Since(started)}
}
@@ -348,7 +348,7 @@ func syncResource(c interface {
// Single object response - try to store as-is
if err := upsertSingleObject(db, resource, data); err != nil {
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
}
return syncResult{Resource: resource, Err: err, Duration: time.Since(started)}
}
@@ -370,7 +370,7 @@ func syncResource(c interface {
stored, extractFailures, err := upsertResourceBatch(db, resource, items)
if err != nil {
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_error","resource":"%s","error":"%s"}`+"\n", resource, strings.ReplaceAll(err.Error(), `"`, `\"`))
}
return syncResult{Resource: resource, Count: totalCount, Err: fmt.Errorf("upserting batch for %s: %w", resource, err), Duration: time.Since(started)}
}
@@ -387,7 +387,7 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "warning: %s returned %d items but stored 0 — the local store will be empty for this resource. Likely cause: scalar item shape rather than objects with extractable IDs.\n", resource, len(items))
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"reason":"all_items_failed_id_extraction"}`+"\n", resource, len(items))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"reason":"all_items_failed_id_extraction"}`+"\n", resource, len(items))
}
anomalyEmitted = true
} else if extractFailures > 0 && !anomalyEmitted {
@@ -398,7 +398,7 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\nwarning: %s had %d item(s) on this page with no extractable primary key — those rows were dropped silently. Annotate the spec with x-resource-id to fix.\n", resource, extractFailures)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":%d,"count":%d,"reason":"primary_key_unresolved"}`+"\n", resource, len(items), stored, extractFailures)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":%d,"count":%d,"reason":"primary_key_unresolved"}`+"\n", resource, len(items), stored, extractFailures)
}
anomalyEmitted = true
}
@@ -416,9 +416,9 @@ func syncResource(c interface {
}
} else {
if currentRate > 0 {
- fmt.Fprintf(os.Stderr, `{"event":"sync_progress","resource":"%s","fetched":%d,"rate_rps":%.1f}`+"\n", resource, atomic.LoadInt64(&progressCount), currentRate)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_progress","resource":"%s","fetched":%d,"rate_rps":%.1f}`+"\n", resource, atomic.LoadInt64(&progressCount), currentRate)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_progress","resource":"%s","fetched":%d}`+"\n", resource, atomic.LoadInt64(&progressCount))
+ fmt.Fprintf(os.Stdout, `{"event":"sync_progress","resource":"%s","fetched":%d}`+"\n", resource, atomic.LoadInt64(&progressCount))
}
}
@@ -435,7 +435,7 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\n %s: reached --max-pages limit (%d pages, %d items)\n", resource, maxPages, totalCount)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","reason":"max_pages_cap_hit","message":"reached --max-pages cap of %d; data may be truncated. Re-run with --max-pages 0 (unlimited) or higher to verify."}`+"\n", resource, maxPages)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","reason":"max_pages_cap_hit","message":"reached --max-pages cap of %d; data may be truncated. Re-run with --max-pages 0 (unlimited) or higher to verify."}`+"\n", resource, maxPages)
}
break
}
@@ -450,7 +450,7 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\n %s: API returned the same next cursor across two pages; aborting to prevent budget waste.\n", resource)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_warning","resource":"%s","reason":"stuck_pagination","message":"API returned the same next cursor across two pages for resource %s; aborting to prevent budget waste."}`+"\n", resource, resource)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_warning","resource":"%s","reason":"stuck_pagination","message":"API returned the same next cursor across two pages for resource %s; aborting to prevent budget waste."}`+"\n", resource, resource)
}
break
}
@@ -478,12 +478,12 @@ func syncResource(c interface {
if humanFriendly {
fmt.Fprintf(os.Stderr, "\nwarning: %s consumed %d items, extracted %d primary keys, but stored 0 rows — extraction succeeded yet nothing landed. Investigate FTS triggers / transaction rollback / encoding.\n", resource, consumedTotal, consumedTotal-extractFailureTotal)
} else {
- fmt.Fprintf(os.Stderr, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"extract_failures":%d,"reason":"stored_count_zero_after_extraction"}`+"\n", resource, consumedTotal, extractFailureTotal)
+ fmt.Fprintf(os.Stdout, `{"event":"sync_anomaly","resource":"%s","consumed":%d,"stored":0,"extract_failures":%d,"reason":"stored_count_zero_after_extraction"}`+"\n", resource, consumedTotal, extractFailureTotal)
}
}
if !humanFriendly {
- fmt.Fprintf(os.Stderr, `{"event":"sync_complete","resource":"%s","total":%d,"duration_ms":%d}`+"\n", resource, totalCount, time.Since(started).Milliseconds())
+ fmt.Fprintf(os.Stdout, `{"event":"sync_complete","resource":"%s","total":%d,"duration_ms":%d}`+"\n", resource, totalCount, time.Since(started).Milliseconds())
}
return syncResult{Resource: resource, Count: totalCount, Duration: time.Since(started)}
← 37ee7e38 fix(cli): stamp run_id into manifest at generate time (#606)
·
back to Cli Printing Press
·
fix(cli): emit ClawHub-compliant nested YAML for SKILL.md me 4897c593 →