Skip to content

fix(cli): reduce over-budget projected list pages instead of shortening values - #175

Merged
ysyneu merged 1 commit into
mainfrom
fix/list-auto-reduce-page-fit
Aug 29, 2026
Merged

fix(cli): reduce over-budget projected list pages instead of shortening values#175
ysyneu merged 1 commit into
mainfrom
fix/list-auto-reduce-page-fit

Conversation

@ysyneu

@ysyneu ysyneu commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Problem

fduty alert-event list --limit 100 --json (and incident list, incident similar, channel escalate-rule-list) bound their projected structured output to the 16 KiB budget by fair-capping every shortenable string value across every row, marking clipped ones with .... At a large --limit the per-value cap collapses to a few bytes, so the page comes back as 100 rows of x... stubs.

For a --json/--toon consumer this is silent corruption: a jq select(.title | test(...)) or an exact id match runs over the clipped values, misses rows that really match, and an empty result is indistinguishable from a genuine non-match. A stderr note announced the shortening, but the clipped page itself was still the output.

Root cause

boundProjectedList (internal/cli/fieldproject.go) had exactly one tool for an over-budget page: shrink values until all rows fit. It never considered shrinking the page itself — the dimension that costs the consumer nothing, because these commands are already paginated (--limit/--page).

Fix

  • On overflow, the page is reduced first: emit the largest leading prefix of rows that fits the budget with every value byte-intact (largestFittingPrefix, a binary search — prefix encoded size is monotone), and announce the reduction on stderr:
    note: emitted N of M projected rows (every value intact) to stay below the 16384-byte structured-output limit; narrow --fields or lower --limit to fit more rows per page — the rows past the first N were not emitted
  • Value shortening is kept only for the case page reduction cannot solve: a single row that overflows the budget on its own. Identifier fields stay exempt, every shortened value keeps the ... marker, and the irreducible case (nothing shortenable) still fails with the error naming the largest fields.
  • boundProjectedOutput now returns the bounded data (same type as its input) alongside the note; the four list call sites use the returned slice, and the two paginated ones (alert-event list, incident list) pass the emitted count and effective limit to PrintList so its pagination note stays accurate.
  • --fields flag help and the alert/incident skill cards now describe the page-reduction semantics.

Notes for reviewers

  • Behavior change for --json/--toon list consumers: a large --limit now yields a smaller, fully intact page instead of ...-clipped rows (the point of this change). Scripts that count array length as the page size must heed the stderr notes.
  • PrintList's generic raise --limit or use --page note is unchanged (shared helper, wider blast radius); the specific reduction note governs.
  • alert list --fields never bounds its projection at all — pre-existing gap, deliberately out of scope.

Verification

$ go test ./internal/cli/ -run 'BoundProjected|AlertEventList|IncidentList|IncidentSimilar|ChannelEscalateRule|FieldsProjection|StructuredFieldsEmpty' -v
... (all pass, including:)
--- PASS: TestAlertEventListAutoReducesPageAtLargeLimit (json, toon)
--- PASS: TestIncidentListAutoReducesPageAtLargeLimit (json, toon)
ok  github.com/flashcatcloud/flashduty-cli/internal/cli

$ make check-cards
skilldoc: cards OK

$ make check   # gofmt/gci, golangci-lint (0 issues), go test -race ./..., build
ok  github.com/flashcatcloud/flashduty-cli/cmd/flashduty
ok  github.com/flashcatcloud/flashduty-cli/internal/cli
... (all packages ok, build succeeds)

The two new AutoReducesPage tests stub 100 rows with ~600-byte mixed CJK titles via the httptest stub and run alert-event list --limit 100 / incident list --limit 100 in both json and toon: exit 0, stdout parses as an array of ≥1 and <100 rows forming the leading prefix of the fixture with every value byte-identical, no ... anywhere, output under 16 KiB, and stderr carries the reduction note naming the emitted count.

TestCommandListProjectionOverflowFails and the end-to-end TestProjectionOverflowFailsHard previously pinned error-on-overflow with multi-row fixtures; those pages now reduce (covered above), so both were re-pointed at the case that still must fail: a single row whose projected non-string field alone exceeds the budget (non-zero exit, empty stdout, error naming the largest fields). Manual eyeball: alert-event list --help shows the new --fields sentence.

…ng values

When a projected list page (alert-event list, incident list, incident
similar, channel escalate-rule-list) overflows the 16 KiB structured-output
budget, the previous behavior fair-capped every shortenable string value
across every row and marked the clipped ones with "...". A --json/--toon
consumer filtering or exact-matching on those values (jq select, id
comparisons) silently misses rows that really match, and an empty result is
indistinguishable from a genuine non-match.

boundProjectedList now reduces the page first: it emits the largest leading
prefix of rows that fits the budget with every value byte-intact
(largestFittingPrefix, a binary search over the monotone prefix size), and
announces the emitted count on stderr. Value shortening is kept only for
the case page reduction cannot solve — a single row that overflows the
budget on its own — with identifier fields still exempt and the "..."
marker still guaranteed; the irreducible case still fails with the error
naming the largest fields.

boundProjectedOutput returns the bounded data (same type) alongside the
note; the four list call sites use the returned slice, and the two
paginated ones pass the emitted count and effective limit to PrintList so
its pagination note stays accurate. Flag help and the alert/incident skill
cards describe the page-reduction semantics.
@ysyneu
ysyneu merged commit abd120d into main Aug 29, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant