fix(read): lean envelopes for every list/search projection and exact nested get selectors
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.
| Item | Issue | Change |
|---|---|---|
| pm-gh1674 | Closes #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-gh1663 | Closes #1663 | linked.<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)
| command | before | after |
|---|---|---|
pm list --status open --limit 3 (brief) | 962 B | 951 B |
pm list --status open --limit 3 --fields id,title | 1,296 B | 887 B |
pm list --status open --limit 3 --compact | 1,274 B | 957 B |
pm search "release workflow" --limit 3 --fields id,title | 1,036 B | 444 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,compactorfieldsplus 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
--fieldspage keeps it, because no row shows them. filters.fieldsis dropped when it names exactly the projection. On--fieldssearch pages,nowand amode_sourceother thansettingsare dropped too.- SDK
runSearchwith afieldsprojection now echoes filters through the same set-only summary the compact default uses.--fullkeeps 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.
linkedcarries only the selected groups; a present empty array now always means the group is empty.claim_state.<member>selects that member, the same wayschedule.<member>already did. The wholelinked/claim_stateselector and depth reads are unchanged.- Field projections register the populated siblings a nested selector left out (
linked.tests,claim_state.assignee,schedule.reminders, …). Null,falseand[]siblings withhold nothing, so they are not listed. - SDK contract change, acknowledged in
sdk/public-surface.json:GetResult.linkedmembers are optional andGetResult.claim_stateisPartial<ClaimStateContext>. Migration: readlinked.<group>with optional chaining or?? [], or request the wholelinkedselector. 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.tsruns the built CLI (TOON and JSON) and MCPpm_geton a real sandbox tracker. It asserts that the--fields/--compactenvelopes are no larger than the brief one, thatsearch --fieldsfilters 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.fieldsecho matching,mode_source, requested-empty[], and the claim/schedule member receipts. - Negative controls: reverting
list-envelope.tsfails 4 envelope specs, and restoring the linked padding fails 4 get-receipt specs. - Local: full
quality:staticchain (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.linkedmembers (files,tests,docs) are now optional, andGetResult.claim_stateisPartial<ClaimStateContext>. Readlinked.<group>with optional chaining or?? []; requesting the wholelinkedselector still returns every group.- Populated siblings left out by a nested selector are named in the omission receipt as
<facet>.<member>with arestore_with. runSearchwith afieldsprojection echoes only the filters the caller set;--fullkeeps the verbose echo.
Bug Fixes
- A sibling already returned through its same-named item metadata field (such as
item.testsfor--fields linked.files,tests) is no longer reported as withheld; derived members likeschedule.start_atandclaim_state.last_claimhave 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).