Skip to content

fix(cli): keep identifier fields intact in list projection shortening - #173

Merged
ysyneu merged 1 commit into
mainfrom
fix/projection-keep-identifiers
Aug 28, 2026
Merged

fix(cli): keep identifier fields intact in list projection shortening#173
ysyneu merged 1 commit into
mainfrom
fix/projection-keep-identifiers

Conversation

@ysyneu

@ysyneu ysyneu commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Defect

The compact list projections (incident list, incident similar, alert-event list in --output-format json|toon) bound their output to the 16 KiB list budget by applying one "fair" per-field byte cap to every string value. When a page overflows badly enough that the fair cap lands below an identifier's own length, identifier fields — incident_id, alert_id, event_id, alert_key, … — are clipped mid-value with a trailing ....

User-visible symptom: a consumer that filters the JSON output by exact match (jq 'select(.alert_key == "...")') or passes an id back into a follow-up detail call gets nothing — a clipped identifier matches no real value, and the empty result is indistinguishable from "nothing matched". Whether an id survives depends on what else happened to overflow on that page, so exact-match filtering over list output is unreliable by default.

Root cause

boundProjectedList (internal/cli/fieldproject.go) computed maxLen, ran its fits() trials, and applied the final cap uniformly over all string values — no distinction between identifiers (values a consumer matches, filters, or passes back verbatim) and free text (values a human reads).

Fix

Add isIdentifierField(key) (keys ending in _id or _key) and exempt those fields at all three points of boundProjectedList:

  1. the maxLen sizing scan skips identifier keys;
  2. fits() copies identifier values verbatim into the trial rows;
  3. the apply loop skips identifier keys before total++, so the shortening note's denominator counts only shortenable strings.

Identifiers now survive byte-intact — or, when a page's irreducible content (identifier strings + keys + envelope) alone exceeds the budget, the existing narrowing error fires (request fewer rows (--limit) or fewer --fields) instead of shipping clipped ids. Note format and error text are unchanged; boundProjectedDetail, all projection call sites, flag help, and skill docs are untouched.

Tests

  • TestBoundProjectedListNeverShortensIdentifierFields (json + toon subtests): 10 rows of {event_id, alert_key, title(500B)} at a 1400-byte budget — both id fields byte-identical in every row, every title shortened with the ... marker, the note names title but not the identifier fields, and the re-marshaled output fits the budget. Against the old implementation the json subtest fails: the fair cap lands at 27 bytes and clips the 32-byte alert_key.
  • TestBoundProjectedListIdentifierOnlyOverflowErrors: 12 rows of {incident_id} at a 512-byte budget — errors with "request fewer rows" and leaves every row unmutated. Against the old implementation it fails: the old code clipped every 24-byte id to 12 bytes and returned success.
  • TestBoundProjectedListNeverEmitsUnmarkedTruncation (pre-existing): its 100-row fixture only ever fit because the old code clipped the ids to fit the budget — under the exemption that page errors by design, so the fixture is resized to 90 rows, keeping it in the "titles shorten, ids intact" regime the test is about. This is the intended contract: a page whose irreducible identifier content alone exceeds the budget now fails with the narrowing error instead of shipping mangled ids.

Verification

$ go test ./internal/cli/ -run 'Projection|BoundProjected'
ok  	github.com/flashcatcloud/flashduty-cli/internal/cli	0.808s

$ go test -race ./internal/cli/
ok  	github.com/flashcatcloud/flashduty-cli/internal/cli	5.758s

$ make check   # gofmt -s + gci, golangci-lint, go test -race ./..., go build
bin/golangci-lint run --allow-serial-runners
0 issues.
go test -race ./...
ok  	github.com/flashcatcloud/flashduty-cli/cmd/flashduty	6.258s
ok  	github.com/flashcatcloud/flashduty-cli/internal/cli	(cached)
ok  	github.com/flashcatcloud/flashduty-cli/internal/cmd/cligen	1.566s
ok  	github.com/flashcatcloud/flashduty-cli/internal/cmd/skilldoc	2.963s
ok  	github.com/flashcatcloud/flashduty-cli/internal/config	1.516s
ok  	github.com/flashcatcloud/flashduty-cli/internal/output	2.334s
ok  	github.com/flashcatcloud/flashduty-cli/internal/skilldoc	2.663s
ok  	github.com/flashcatcloud/flashduty-cli/internal/timeutil	1.776s
ok  	github.com/flashcatcloud/flashduty-cli/internal/update	2.937s
go build -v ... -o bin/flashduty ./cmd/flashduty

boundProjectedList applied one fair byte cap to every string value, so
an overflowing page silently clipped identifier fields (keys ending in
_id/_key, e.g. alert_key, incident_id) mid-value. A clipped identifier
silently defeats the consumer's exact-match filter or follow-up detail
call: the query matches nothing and looks like an empty result.

Add isIdentifierField and exempt _id/_key fields at all three points of
boundProjectedList: the maxLen sizing scan, the fits() trials, and the
final apply loop (skipped before the note's denominator, so it counts
only shortenable strings). Identifiers now survive byte-intact; when a
page's irreducible content alone overflows the budget, the existing
narrowing error fires instead of shipping clipped ids.
@ysyneu
ysyneu merged commit d5dca68 into main Aug 28, 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