fix(read): lean envelopes for every list/search projection and exact nested get selectors

Merged
UN
6d ago
+897-131

Summary

Two agent-UX defects from the same family. In both, a narrow read told the agent something other than what it asked for. GH-1674: a narrower list or search projection cost more envelope than the default. GH-1663: a nested get selector padded the members it was not asked for with empty arrays, so they read as evidence that the groups were empty.

ItemIssueChange
pm-gh1674Closes #1674--fields, --compact and every projected pm search page share the lean TOON envelope of the default brief page. search --fields echoes only the filters the caller set (TOON and JSON).
pm-gh1663Closes #1663linked.<group>, claim_state.<member> and schedule.<member> return only the members they name. The omission receipt names the populated siblings they left out as <facet>.<member>, each with a narrow restore_with.
pm-sj66lm— (filed, not fixed here)Follow-up found while fixing GH-1663: in TOON, a complete selection receipt costs four lines, and a requested empty collection disappears.

Measured (this repository's tracker, 3,211 items)

commandbeforeafter
pm list --status open --limit 3 (brief)962 B951 B
pm list --status open --limit 3 --fields id,title1,296 B887 B
pm list --status open --limit 3 --compact1,274 B957 B
pm search "release workflow" --limit 3 --fields id,title1,036 B444 B

GH-1674: one lean envelope for every list and search projection

projectPmBriefListEnvelope recognized only mode: brief with the exact brief field list. projectPmBriefSearchEnvelope bailed out whenever a projection key existed. Every other projection therefore fell through to the verbose shape: truncated, now, the projection echo, filters.limit/filters.fields echoes, the four-row effective_keys array and the four-line omission receipt.

  • Both projections now recognize a standard row projection: mode brief, compact or fields plus a non-empty field list. Every row must carry exactly the projected keys; a producer whose echo disagrees with its rows passes through unchanged, so the compaction never hides a mismatch.
  • The projection echo is dropped because the TOON row header already lists the fields. An empty --fields page keeps it, because no row shows them.
  • filters.fields is dropped when it names exactly the projection. On --fields search pages, now and a mode_source other than settings are dropped too.
  • SDK runSearch with a fields projection now echoes filters through the same set-only summary the compact default uses. --full keeps the complete resolved echo.

Deliberately unchanged, with reasons on the item: completeness keeps its one status line (pm-6pycus: unchecked source evidence never disappears), and items_encoding keeps json_columns/absent. Both the repo's TOON decoder spec and external hosts detect an encoding block by those two arrays. List JSON is unchanged, because default and --fields JSON already differed only by the filters.fields echo.

GH-1663: nested get selectors return only what they name

Two defects together produced the padded siblings. First, attachGetLinked kept a stable three-group shape and filled unselected groups with []. Second, runGet registered an empty material set for every explicit --fields read, so the receipt could never report what was left out.

  • linked carries only the selected groups; a present empty array now always means the group is empty. claim_state.<member> selects that member, the same way schedule.<member> already did. The whole linked/claim_state selector and depth reads are unchanged.
  • Field projections register the populated siblings a nested selector left out (linked.tests, claim_state.assignee, schedule.reminders, …). Null, false and [] siblings withhold nothing, so they are not listed.
  • SDK contract change, acknowledged in sdk/public-surface.json: GetResult.linked members are optional and GetResult.claim_state is Partial<ClaimStateContext>. Migration: read linked.<group> with optional chaining or ?? [], or request the whole linked selector. The before/after/migrate line is also recorded on pm-gh1657, which owns the release-notes contract-change section.

Verification

  • New tests/integration/read-output/projection-envelopes.integration.spec.ts runs the built CLI (TOON and JSON) and MCP pm_get on a real sandbox tracker. It asserts that the --fields/--compact envelopes are no larger than the brief one, that search --fields filters contain only set keys, and that nested get selectors return only what they name and disclose siblings.
  • Unit specs cover every new branch: consistency refusals, the empty-page projection, filters.fields echo matching, mode_source, requested-empty [], and the claim/schedule member receipts.
  • Negative controls: reverting list-envelope.ts fails 4 envelope specs, and restoring the linked padding fails 4 get-receipt specs.
  • Local: full quality:static chain (minus mutation) and the full coverage suite.

Summary by Sourcery

Ensure read projections disclose only the requested data while keeping list and search responses concise.

Bug Fixes:

  • Make list and search field or compact projections use lean envelopes comparable to the default brief output, while preserving full output when requested.
  • Return only explicitly selected nested get members and report populated omitted siblings with actionable restoration selectors.
  • Echo only caller-specified filters for projected searches and update the SDK contract for optional linked and partial claim-state results.

Enhancements:

  • Align omission receipts and nested selectors across linked, claim-state, and schedule facets, including requested empty collections and metadata aliases.

Documentation:

  • Document lean projection envelopes, nested selector behavior, omission receipts, and the updated SDK contract.

Tests:

  • Add unit and integration coverage for lean list/search envelopes, filter echoes, nested get projections, omission receipts, TOON/JSON output, and MCP responses.

Chores:

  • Record the associated issue, history, reminder, changelog, and release-contract updates.

Summary by cubic

Fixes two read-projection defects: pm list and pm search projections now share the lean envelope of the default page, and nested get selectors return only the members they name instead of padding unrequested groups with empty arrays.

Breaking change

  • GetResult.linked members (files, tests, docs) are now optional, and GetResult.claim_state is Partial<ClaimStateContext>. Read linked.<group> with optional chaining or ?? []; requesting the whole linked selector still returns every group.
  • Populated siblings left out by a nested selector are named in the omission receipt as <facet>.<member> with a restore_with.
  • runSearch with a fields projection echoes only the filters the caller set; --full keeps the verbose echo.

Bug Fixes

  • A sibling already returned through its same-named item metadata field (such as item.tests for --fields linked.files,tests) is no longer reported as withheld; derived members like schedule.start_at and claim_state.last_claim have no alias and are still named.
  • The hit, empty, and count-only search response paths now share one filter-echo selection.

Also filed follow-ups not fixed here: pm-sj66lm (in TOON, a complete omission receipt costs four lines and a requested empty collection disappears) and pm-hka7ep (comment edit/delete indexes don't match a budget-truncated listing). The release-health reminder is re-armed and date-independent, tracking the latest verified cut (v2026.11.6).