← back to Cli Printing Press
fix(cli): preserve unknown-shape records in compactListFields (#1078)
dc6fe8791ec3693a8addac8dab27f0e333c1e326 · 2026-05-11 09:51:32 -0700 · Trevin Chow
`compactListFields` previously reduced any list item that carried no
allowlist key to `{}`, so printed CLIs whose APIs use domain-specific
field names (travel: fare/airline; commerce: vendor; locale: domain)
silently emitted empty objects under `--agent` / `--compact`. The narrow
id/name/title allowlist also dropped useful business scalars (price,
rating, currency) from canonical-schema records that did match `id`/`name`.
Extend the allowlist with monetary, metric, locale, identity-adjacent,
temporal, and versioning scalars, and add a per-item fallback so records
with no matching keys are preserved whole instead of nuked. Pin the
contract via a new template-level test mirroring TestPipedJsonGate.
Closes #1046
Files touched
M internal/generator/generator_test.goM internal/generator/templates/helpers.go.tmplM testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.goM testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
Diff
commit dc6fe8791ec3693a8addac8dab27f0e333c1e326
Author: Trevin Chow <trevin@trevinchow.com>
Date: Mon May 11 09:51:32 2026 -0700
fix(cli): preserve unknown-shape records in compactListFields (#1078)
`compactListFields` previously reduced any list item that carried no
allowlist key to `{}`, so printed CLIs whose APIs use domain-specific
field names (travel: fare/airline; commerce: vendor; locale: domain)
silently emitted empty objects under `--agent` / `--compact`. The narrow
id/name/title allowlist also dropped useful business scalars (price,
rating, currency) from canonical-schema records that did match `id`/`name`.
Extend the allowlist with monetary, metric, locale, identity-adjacent,
temporal, and versioning scalars, and add a per-item fallback so records
with no matching keys are preserved whole instead of nuked. Pin the
contract via a new template-level test mirroring TestPipedJsonGate.
Closes #1046
---
internal/generator/generator_test.go | 33 ++++++++++++++++++++++
internal/generator/templates/helpers.go.tmpl | 26 +++++++++++++++--
.../internal/cli/helpers.go | 26 +++++++++++++++--
.../printing-press-golden/internal/cli/helpers.go | 26 +++++++++++++++--
4 files changed, 105 insertions(+), 6 deletions(-)
diff --git a/internal/generator/generator_test.go b/internal/generator/generator_test.go
index d9b15871..a60d65f7 100644
--- a/internal/generator/generator_test.go
+++ b/internal/generator/generator_test.go
@@ -3332,6 +3332,39 @@ func TestGeneratedOutput_MutatingCommandsHaveEnvelope(t *testing.T) {
assert.Contains(t, content, `envelope["success"] = false`)
}
+// TestCompactListFieldsPreservesUnknownShapes pins the two contracts that
+// keep `--agent` / `--compact` from silently emitting {} for records that
+// don't match the canonical id/name/title allowlist: a per-item fallback
+// to the original item, and a wider scalar allowlist that covers
+// monetary, metric, locale, and identity-adjacent keys.
+//
+// Pinned at the template level because the helper renders into every
+// printed CLI's helpers.go and a per-fixture assertion would miss future
+// copies that drift in.
+func TestCompactListFieldsPreservesUnknownShapes(t *testing.T) {
+ t.Parallel()
+
+ path := filepath.Join("templates", "helpers.go.tmpl")
+ data, err := os.ReadFile(path)
+ require.NoError(t, err, "template must exist: %s", path)
+ body := string(data)
+
+ assert.Contains(t, body, "if len(compact) == 0 {",
+ "compactListFields must preserve the original item when no allowlist keys match — otherwise records with domain-specific field names get reduced to {}")
+ assert.Contains(t, body, "compact = item",
+ "compactListFields must assign the original item as the per-item fallback")
+
+ for _, key := range []string{
+ `"price":`, `"fare":`, `"currency":`,
+ `"rating":`,
+ `"locale":`, `"language":`,
+ `"code":`,
+ } {
+ assert.Contains(t, body, key,
+ "compactListFields allowlist must include %s so canonical-schema records keep high-signal scalars across business/travel/commerce/locale APIs", key)
+ }
+}
+
// TestPipedJsonGateRespectsExplicitFormatFlags pins the contract: the
// piped-output auto-JSON gate must defer to explicit --csv / --quiet /
// --plain flags so piped consumers that asked for a non-JSON format
diff --git a/internal/generator/templates/helpers.go.tmpl b/internal/generator/templates/helpers.go.tmpl
index 976511d3..a5c2fcba 100644
--- a/internal/generator/templates/helpers.go.tmpl
+++ b/internal/generator/templates/helpers.go.tmpl
@@ -777,12 +777,31 @@ func compactFields(data json.RawMessage) json.RawMessage {
}
// compactListFields keeps only high-gravity fields for array responses.
+// When an item carries none of these keys the original is preserved, so
+// `--agent` does not silently emit {} for APIs whose response shapes use
+// domain-specific field names (travel: airline/destination; commerce: vendor).
func compactListFields(items []map[string]any) json.RawMessage {
keepFields := map[string]bool{
+ // Identity
"id": true, "name": true, "title": true, "identifier": true,
- "status": true, "state": true, "type": true, "priority": true,
- "url": true, "email": true, "key": true,
+ "code": true, "slug": true, "key": true,
+ // Categorization
+ "status": true, "state": true, "type": true, "kind": true, "priority": true,
+ // Communication
+ "url": true, "email": true,
+ // Monetary
+ "price": true, "amount": true, "cost": true, "fare": true,
+ "rate": true, "currency": true,
+ // Metrics
+ "rating": true, "score": true, "count": true,
+ // Locale / geo
+ "language": true, "locale": true, "country": true, "region": true,
+ "city": true, "domain": true,
+ // Temporal
"created_at": true, "updated_at": true, "createdAt": true, "updatedAt": true,
+ "date": true,
+ // Versioning
+ "version": true,
}
filtered := make([]map[string]any, 0, len(items))
@@ -793,6 +812,9 @@ func compactListFields(items []map[string]any) json.RawMessage {
compact[k] = v
}
}
+ if len(compact) == 0 {
+ compact = item
+ }
filtered = append(filtered, compact)
}
result, _ := json.Marshal(filtered)
diff --git a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go
index 5ed45def..5d0bf0c8 100644
--- a/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go
+++ b/testdata/golden/expected/generate-golden-api-rich-auth/printing-press-rich-auth/internal/cli/helpers.go
@@ -579,12 +579,31 @@ func compactFields(data json.RawMessage) json.RawMessage {
}
// compactListFields keeps only high-gravity fields for array responses.
+// When an item carries none of these keys the original is preserved, so
+// `--agent` does not silently emit {} for APIs whose response shapes use
+// domain-specific field names (travel: airline/destination; commerce: vendor).
func compactListFields(items []map[string]any) json.RawMessage {
keepFields := map[string]bool{
+ // Identity
"id": true, "name": true, "title": true, "identifier": true,
- "status": true, "state": true, "type": true, "priority": true,
- "url": true, "email": true, "key": true,
+ "code": true, "slug": true, "key": true,
+ // Categorization
+ "status": true, "state": true, "type": true, "kind": true, "priority": true,
+ // Communication
+ "url": true, "email": true,
+ // Monetary
+ "price": true, "amount": true, "cost": true, "fare": true,
+ "rate": true, "currency": true,
+ // Metrics
+ "rating": true, "score": true, "count": true,
+ // Locale / geo
+ "language": true, "locale": true, "country": true, "region": true,
+ "city": true, "domain": true,
+ // Temporal
"created_at": true, "updated_at": true, "createdAt": true, "updatedAt": true,
+ "date": true,
+ // Versioning
+ "version": true,
}
filtered := make([]map[string]any, 0, len(items))
@@ -595,6 +614,9 @@ func compactListFields(items []map[string]any) json.RawMessage {
compact[k] = v
}
}
+ if len(compact) == 0 {
+ compact = item
+ }
filtered = append(filtered, compact)
}
result, _ := json.Marshal(filtered)
diff --git a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
index 80b431b3..8a2aa5b3 100644
--- a/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
+++ b/testdata/golden/expected/generate-golden-api/printing-press-golden/internal/cli/helpers.go
@@ -582,12 +582,31 @@ func compactFields(data json.RawMessage) json.RawMessage {
}
// compactListFields keeps only high-gravity fields for array responses.
+// When an item carries none of these keys the original is preserved, so
+// `--agent` does not silently emit {} for APIs whose response shapes use
+// domain-specific field names (travel: airline/destination; commerce: vendor).
func compactListFields(items []map[string]any) json.RawMessage {
keepFields := map[string]bool{
+ // Identity
"id": true, "name": true, "title": true, "identifier": true,
- "status": true, "state": true, "type": true, "priority": true,
- "url": true, "email": true, "key": true,
+ "code": true, "slug": true, "key": true,
+ // Categorization
+ "status": true, "state": true, "type": true, "kind": true, "priority": true,
+ // Communication
+ "url": true, "email": true,
+ // Monetary
+ "price": true, "amount": true, "cost": true, "fare": true,
+ "rate": true, "currency": true,
+ // Metrics
+ "rating": true, "score": true, "count": true,
+ // Locale / geo
+ "language": true, "locale": true, "country": true, "region": true,
+ "city": true, "domain": true,
+ // Temporal
"created_at": true, "updated_at": true, "createdAt": true, "updatedAt": true,
+ "date": true,
+ // Versioning
+ "version": true,
}
filtered := make([]map[string]any, 0, len(items))
@@ -598,6 +617,9 @@ func compactListFields(items []map[string]any) json.RawMessage {
compact[k] = v
}
}
+ if len(compact) == 0 {
+ compact = item
+ }
filtered = append(filtered, compact)
}
result, _ := json.Marshal(filtered)
← d7e2d74f feat(cli): promote html_scrape reachability mode when captch
·
back to Cli Printing Press
·
feat(cli): sync.walker spec parameter for hierarchical APIs 24962b7c →