Skip to content

feat: add framework-level output projection with --full#1537

Open
luozhixiong01 wants to merge 1 commit into
mainfrom
feat/output-projection-shortcut
Open

feat: add framework-level output projection with --full#1537
luozhixiong01 wants to merge 1 commit into
mainfrom
feat/output-projection-shortcut

Conversation

@luozhixiong01

@luozhixiong01 luozhixiong01 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a framework-level (domain-agnostic) output-projection capability to the shortcut runtime: a Projectable shortcut declares an OutputSchema, the runtime trims the default view to the curated whitelist, and a boolean --full escape hatch restores the complete upstream payload. The default view is a positive whitelist — fields not declared (including future upstream additions) are full-only. im read shortcuts are the first adopters (verbose fields like chat avatar URLs, tenant_key, and message sender internals waste agent context); other domains' read shortcuts are unchanged, since projection is opt-in per shortcut. Measured: +chat-list default is ~46% smaller than --full.

Changes

  • Add projection engine shortcuts/common/projection.go (ProjectBySchema normalizes to canonical JSON, then keeps only Projected fields; KeepFields/ArrayOf/ObjectOf builders) + unit tests projection_test.go
  • Wire projection into the runner: shortcuts/common/runner.go (emit/outFormat apply projection, register --full, jq-miss stderr hint), types.go (Projectable/OutputSchema/NoFullViewHint), internal/schema/types.go (Projected tag), internal/output/jq.go (jq filter counts)
  • Declare OutputSchema on 5 im read shortcuts: +chat-list, +chat-search, +flag-list, +feed-shortcut-list, +feed-group-list
  • Tighten message sender to {id, sender_type, name} and decouple name resolution in shortcuts/im/convert_lib/{content_convert,helpers}.go; add a no-full redirect hint on 4 message commands
  • Update 9 skills/lark-im/references/*.md (curated-view / --full --jq usage) and add a curated-view/--full convention to skills/lark-shared/SKILL.md
  • Scope: projection is opt-in via OutputSchema; only the 5 im read shortcuts above adopt it in this PR — all other domains' read output is unchanged

Test Plan

  • make unit-test passed
  • validate passed (build / vet / unit / integration)
  • local-eval (sandbox): E2E 10/10 passed; skillave 6/7 — the 1 fail is an out-of-scope generic-delivery case (all projection decision points pass), accepted at human gate
  • acceptance-reviewer passed (8/8 cases)
  • manual verification: lark-cli im +chat-list --as user --page-size 20 --format json returns 6554 B (curated) vs 12146 B with --full (46% smaller default); --full --jq '.data.chats[].avatar' retrieves the hidden field

Related Issues

N/A

Summary by CodeRabbit

  • New Features

    • Added --full flag support to reveal complete output for chat-list, chat-search, feed-group-list, feed-shortcut-list, and flag-list commands.
    • Commands now display curated default views showing essential fields; use --full to access all available data.
    • Added guidance messages suggesting --full when jq filters reference fields not in the default view.
  • Documentation

    • Updated reference docs explaining curated default views versus full output for applicable commands.
    • Clarified field availability, minimal sender object shape, and "no full view" constraints for message commands.

Adds a domain-agnostic output-projection engine to the shortcut runtime; a Projectable shortcut declares an OutputSchema and the runtime trims the default view to the curated whitelist, with a boolean --full to restore the full upstream payload. The 5 im read shortcuts are the first adopters; other domains are unchanged (projection is opt-in via OutputSchema). Message senders are tightened to {id, sender_type, name}; adds a jq-miss stderr hint.
@github-actions github-actions Bot added domain/im PR touches the im domain size/L Large or sensitive change across domains or core paths labels Jun 22, 2026
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a schema-driven output projection framework to lark-cli. A new ProjectBySchema engine trims shortcut responses to a curated default view defined by OutputSchema on each Shortcut. The runner gains --full flag wiring and projection-aware --jq guidance. IM shortcuts declare explicit output schemas or NoFullViewHint, sender objects are narrowed in message formatting, and reference docs are updated throughout.

Changes

Output Projection Framework

Layer / File(s) Summary
Schema and Shortcut type contracts
internal/schema/types.go, shortcuts/common/types.go
schema.Property gains a Projected bool field; Shortcut gains Projectable, OutputSchema, and NoFullViewHint fields.
Projection engine and schema builders
shortcuts/common/projection.go, shortcuts/common/projection_test.go
New ProjectBySchema, projectCanonical, canonicalize, droppedFieldNames, anyProjected, and KeepFields/ArrayOf/ObjectOf builders, with comprehensive unit and integration tests.
jq count helpers
internal/output/jq.go
jqFilter returns (count, error); new JqFilterCount and JqFilterRawCount exports surface the count to callers.
Runner projection wiring and --full flag
shortcuts/common/runner.go
RuntimeContext tracks projection state; emit applies projection and emits stderr notes when --jq targets full-only fields; outFormat applies projection across all output formats; runShortcut initializes projection; flag registration adds --full for projectable shortcuts.
IM sender narrowing
shortcuts/im/convert_lib/content_convert.go, shortcuts/im/convert_lib/helpers.go
formatMessageItem narrows sender to {id, sender_type} via projectSender; ResolveSenderNames uses unresolvedUserSenderIDs to collect missing sender IDs by ou_ prefix only.
IM shortcut schema declarations
shortcuts/im/im_chat_list.go, shortcuts/im/im_chat_search.go, shortcuts/im/im_feed_group_list.go, shortcuts/im/im_feed_shortcut_list.go, shortcuts/im/im_flag_list.go, shortcuts/im/im_chat_messages_list.go, shortcuts/im/im_messages_mget.go, shortcuts/im/im_messages_search.go, shortcuts/im/im_threads_messages_list.go
+chat-list, +chat-search, +feed-group-list, +feed-shortcut-list, and +flag-list declare OutputSchema builders; +chat-messages-list, +messages-mget, +messages-search, and +threads-messages-list set NoFullViewHint via a shared constant.
IM reference documentation
skills/lark-im/references/lark-im-chat-list.md, skills/lark-im/references/lark-im-chat-search.md, skills/lark-im/references/lark-im-feed-group-list.md, skills/lark-im/references/lark-im-feed-shortcut-list.md, skills/lark-im/references/lark-im-flag-list.md, skills/lark-im/references/lark-im-chat-messages-list.md, skills/lark-im/references/lark-im-messages-mget.md, skills/lark-im/references/lark-im-messages-search.md, skills/lark-im/references/lark-im-threads-messages-list.md, skills/lark-shared/SKILL.md
Adds "Default view and --full" sections and sender-minimal notes to all affected IM reference docs, plus a shared --full guidance paragraph in SKILL.md.

Sequence Diagram

sequenceDiagram
  participant CLI as CLI User
  participant runner as runner emit
  participant ProjectBySchema as ProjectBySchema
  participant JqFilterCount as JqFilterCount
  participant stderr as stderr

  CLI->>runner: run shortcut with --jq expression
  runner->>ProjectBySchema: project response data
  ProjectBySchema-->>runner: trimmed data
  runner->>JqFilterCount: apply jq expression to trimmed data
  JqFilterCount-->>runner: result count and error
  runner->>stderr: emit full-only field hint when count is zero
  runner-->>CLI: projected stdout output
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • larksuite/cli#638: Modifies the same internal/output/jq.go and shortcuts/common/runner.go files around JqFilterRaw/OutRaw jq routing, directly overlapping with the JqFilterCount and projection-aware jq changes here.
  • larksuite/cli#1095: Modifies shortcuts/im/convert_lib/content_convert.go's formatMessageItem output map (conditionally emitting update_time), directly adjacent to the sender projection change in this PR.

Suggested labels

domain/im, size/XL

Suggested reviewers

  • YangJunzhou-01
  • liangshuo-1

🐇 Hop! The fields I once showed too many,
Now trimmed to just the ones worth a penny.
With --full you may peek at the rest,
But curated views pass every test!
--jq hints keep confusion at bay —
This bunny ships clean output today! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 67.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: add framework-level output projection with --full' accurately summarizes the main change: introducing a framework-level projection feature with a --full flag.
Description check ✅ Passed The PR description is comprehensive and well-structured with all required sections: Summary (explaining motivation and scope), Changes (detailed bullet points of all modifications), Test Plan (with checkmarks for verification steps), and Related Issues section.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/output-projection-shortcut
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/output-projection-shortcut

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
shortcuts/common/projection_test.go (1)

178-200: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add an array-item nested case to TestAnyProjected.

Current coverage validates nested object traversal but not Items.Properties. Please add one assertion for a schema where only array-element fields are marked projected, so guard behavior is locked for both object and array trees.

Suggested test addition
 func TestAnyProjected(t *testing.T) {
 	none := &schema.OrderedProps{}
 	none.Set("a", schema.Property{})
 	if anyProjected(none) {
 		t.Fatalf("no marks should report false")
 	}
@@
 	if anyProjected(nil) {
 		t.Fatalf("nil should report false")
 	}
+
+	arrElem := &schema.OrderedProps{}
+	arrElem.Set("id", schema.Property{Projected: true})
+	arr := &schema.OrderedProps{}
+	arr.Set("items", schema.Property{
+		Type:  "array",
+		Items: &schema.Property{Type: "object", Properties: arrElem},
+	})
+	if !anyProjected(arr) {
+		t.Fatalf("array item nested mark should report true")
+	}
 }

As per coding guidelines, every behavior change needs a test alongside the change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/common/projection_test.go` around lines 178 - 200, The
TestAnyProjected function needs an additional test case to verify that the
anyProjected function correctly handles nested array items. Add a new assertion
within the TestAnyProjected function that creates a schema.OrderedProps with a
property of type "array", sets Items with Properties containing a
schema.Property marked with Projected: true, then verifies that anyProjected
returns true for this nested array case. This ensures guard behavior is tested
for both object and array tree traversal, not just object nesting.

Source: Coding guidelines

shortcuts/im/convert_lib/helpers.go (1)

212-213: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Skip already-attempted IDs by key presence, not non-empty value.

Line 212 re-queues IDs when nameMap[id] == "", so previously attempted-but-unresolved users can be queried repeatedly across ResolveSenderNames calls.

Proposed patch
 func unresolvedUserSenderIDs(messages []map[string]interface{}, nameMap map[string]string) []string {
 	var ids []string
 	seen := map[string]bool{}
 	for _, msg := range messages {
 		s, _ := msg["sender"].(map[string]interface{})
 		id, _ := s["id"].(string)
-		if id == "" || !strings.HasPrefix(id, "ou_") || seen[id] || nameMap[id] != "" {
+		if id == "" || !strings.HasPrefix(id, "ou_") || seen[id] {
+			continue
+		}
+		if _, attempted := nameMap[id]; attempted {
 			continue
 		}
 		seen[id] = true
 		ids = append(ids, id)
 	}
 	return ids
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/im/convert_lib/helpers.go` around lines 212 - 213, The condition on
line 212 currently checks nameMap[id] != "" to skip IDs that have been
processed, but this causes previously attempted-but-unresolved users to be
queried repeatedly. Instead of checking for a non-empty value in nameMap, use
Go's comma-ok idiom to check if the key exists in the map. Replace the
nameMap[id] != "" check with a proper key existence check using the pattern _,
exists := nameMap[id], then only skip the ID if the key is present in the map,
regardless of whether the value is empty or not.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@shortcuts/common/projection.go`:
- Around line 132-149: The anyProjected function fails to check for projected
marks nested under array item properties, only examining p.Properties during
recursion. After the recursive call to anyProjected for p.Properties in the
function body, add an additional check that also recursively calls anyProjected
on p.Items.Properties if p.Items is not nil. This ensures that projected fields
within array elements are properly detected and prevents schemas that project
only array element fields from being incorrectly treated as pass-through.

---

Nitpick comments:
In `@shortcuts/common/projection_test.go`:
- Around line 178-200: The TestAnyProjected function needs an additional test
case to verify that the anyProjected function correctly handles nested array
items. Add a new assertion within the TestAnyProjected function that creates a
schema.OrderedProps with a property of type "array", sets Items with Properties
containing a schema.Property marked with Projected: true, then verifies that
anyProjected returns true for this nested array case. This ensures guard
behavior is tested for both object and array tree traversal, not just object
nesting.

In `@shortcuts/im/convert_lib/helpers.go`:
- Around line 212-213: The condition on line 212 currently checks nameMap[id] !=
"" to skip IDs that have been processed, but this causes previously
attempted-but-unresolved users to be queried repeatedly. Instead of checking for
a non-empty value in nameMap, use Go's comma-ok idiom to check if the key exists
in the map. Replace the nameMap[id] != "" check with a proper key existence
check using the pattern _, exists := nameMap[id], then only skip the ID if the
key is present in the map, regardless of whether the value is empty or not.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 79cf9a17-1e58-4e84-b997-8391f898bb1a

📥 Commits

Reviewing files that changed from the base of the PR and between 824aa9e and bbb23f3.

📒 Files selected for processing (27)
  • internal/output/jq.go
  • internal/schema/types.go
  • shortcuts/common/projection.go
  • shortcuts/common/projection_test.go
  • shortcuts/common/runner.go
  • shortcuts/common/types.go
  • shortcuts/im/convert_lib/content_convert.go
  • shortcuts/im/convert_lib/helpers.go
  • shortcuts/im/im_chat_list.go
  • shortcuts/im/im_chat_messages_list.go
  • shortcuts/im/im_chat_search.go
  • shortcuts/im/im_feed_group_list.go
  • shortcuts/im/im_feed_shortcut_list.go
  • shortcuts/im/im_flag_list.go
  • shortcuts/im/im_messages_mget.go
  • shortcuts/im/im_messages_search.go
  • shortcuts/im/im_threads_messages_list.go
  • skills/lark-im/references/lark-im-chat-list.md
  • skills/lark-im/references/lark-im-chat-messages-list.md
  • skills/lark-im/references/lark-im-chat-search.md
  • skills/lark-im/references/lark-im-feed-group-list.md
  • skills/lark-im/references/lark-im-feed-shortcut-list.md
  • skills/lark-im/references/lark-im-flag-list.md
  • skills/lark-im/references/lark-im-messages-mget.md
  • skills/lark-im/references/lark-im-messages-search.md
  • skills/lark-im/references/lark-im-threads-messages-list.md
  • skills/lark-shared/SKILL.md

Comment on lines +132 to +149
// anyProjected reports whether any field in the tree carries Projected==true.
// Used as a guard: a Projectable command whose OutputSchema marks nothing is
// treated as pass-through rather than trimming everything away.
func anyProjected(props *schema.OrderedProps) bool {
if props == nil {
return false
}
for _, key := range props.Order {
p := props.Map[key]
if p.Projected {
return true
}
if anyProjected(p.Properties) {
return true
}
}
return false
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

anyProjected misses projected marks nested under array items.

At Line 144, recursion only checks p.Properties; it ignores p.Items.Properties. That makes anyProjected return false for schemas that project only array element fields, so projection can be incorrectly treated as pass-through.

Proposed fix
 func anyProjected(props *schema.OrderedProps) bool {
 	if props == nil {
 		return false
 	}
 	for _, key := range props.Order {
 		p := props.Map[key]
 		if p.Projected {
 			return true
 		}
-		if anyProjected(p.Properties) {
+		if anyProjected(childProps(p)) {
 			return true
 		}
 	}
 	return false
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@shortcuts/common/projection.go` around lines 132 - 149, The anyProjected
function fails to check for projected marks nested under array item properties,
only examining p.Properties during recursion. After the recursive call to
anyProjected for p.Properties in the function body, add an additional check that
also recursively calls anyProjected on p.Items.Properties if p.Items is not nil.
This ensures that projected fields within array elements are properly detected
and prevents schemas that project only array element fields from being
incorrectly treated as pass-through.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

domain/im PR touches the im domain feature size/L Large or sensitive change across domains or core paths

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant