Skip to content

refactor: add output emitter contract and differential harness#1899

Open
sang-neo03 wants to merge 1 commit into
mainfrom
feat/output-emitter-convergence
Open

refactor: add output emitter contract and differential harness#1899
sang-neo03 wants to merge 1 commit into
mainfrom
feat/output-emitter-convergence

Conversation

@sang-neo03

@sang-neo03 sang-neo03 commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

The success-output path (content-safety scan → envelope → jq → format render → write) has no single owner: it is assembled at two envelope points (RuntimeContext.emit, WriteSuccessEnvelope) and re-implemented across several format-routing functions, which have drifted (raw/Meta support, MarkRaw, the jq safety-warning). This PR lands the foundation for converging them — a single leaf Emitter plus a byte-level differential harness — and is deliberately unwired: no production caller is migrated, so CLI output stays byte-for-byte unchanged and later slices can migrate callers with machine-proven equivalence.

Changes

  • Add internal/output/emitter.go: a command-scoped Emitter that composes the existing leaf primitives (ScanForSafety, Envelope/PrintJson, JqFilter/JqFilterRaw, FormatValue, PaginatedFormatter, WriteAlertWarning) behind Success and StreamPage. It captures out/errOut/identity/color/notice at construction, so emission never reads the global notice hook or os.Stdout.
  • EmitOptions parameterizes the observed differences between the legacy emitters (Raw, OK, Meta, Format, JQ, DryRun, Pretty, JQSafetyWarning); internal/output stays a leaf (no client/cmdutil dependency).
  • Add internal/output/emitter_legacy_diff_test.go: a differential harness that drives the real legacy entry points (RuntimeContext.Out/OutRaw/OutPartialFailure/OutFormat/OutFormatRaw, WriteSuccessEnvelope, the pagination formatter) and asserts byte-identical stdout/stderr and typed errors across raw / ok / meta / jq / notice, all formats, content-safety fail-open/fail-closed, and pagination.

Test Plan

  • make unit-test passed
  • go build ./... / go vet ./... clean; golangci-lint 0 issues; go mod tidy no diff
  • emitter differential harness passes byte-for-byte (GOTOOLCHAIN=go1.25.4 go test -race ./internal/output)
  • skipped: local-eval / acceptance-reviewer — this is an unwired refactor with zero production-caller changes; equivalence is proven by the differential harness
  • manual verification: no production caller instantiates the emitter (grep NewEmitter( finds only the definition); the six production output files show an empty diff vs main; go list -deps ./internal/output excludes client/cmdutil

Related Issues

N/A

Summary by CodeRabbit

  • New Features
    • Introduced a unified output emitter supporting JSON, filtered JSON, pretty rendering, table/CSV, and newline-delimited output.
    • Added structured notices and optional color-aware rendering.
    • Enabled safer streamed pagination with consistent formatting across pages.
    • Integrated configurable content-safety scanning with warn/block behavior.
  • Bug Fixes
    • Prevented incompatible filtering settings during streamed output and added stronger format-change detection across pages.
    • Improved propagation/typing of output and rendering errors.
  • Tests
    • Added legacy “oracle” parity tests to ensure output and error behavior remain consistent.

@sang-neo03 sang-neo03 requested a review from liangshuo-1 as a code owner July 15, 2026 07:05
@github-actions github-actions Bot added the size/L Large or sensitive change across domains or core paths label Jul 15, 2026
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

No new commits to review since the last review.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 307ea5e2-8766-40ea-a57b-07205764533c

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces output.Emitter to centralize success and paginated output across JSON, jq, pretty, formatted, safety, notices, and output validation. Adds legacy-oracle tests covering rendering parity, pagination, dependency injection, safety modes, validation, and typed output errors.

Changes

Output emission

Layer / File(s) Summary
Emitter contracts and dispatch
internal/output/emitter.go
Defines emitter configuration and options, wires notice and renderer dependencies, validates output configuration, and dispatches success output by format.
Emission and pagination paths
internal/output/emitter.go
Implements envelope, pretty, formatted, notice-aware, and paginated emission with safety scanning, warnings, jq restrictions, and stable page formats.
Legacy parity and error validation
internal/output/emitter_legacy_diff_test.go
Compares emitter output and typed errors with legacy paths across formats, safety modes, notices, pagination, pretty rendering, and writer failures.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant Emitter
  participant SafetyProvider
  participant NoticeProvider
  participant OutputWriter
  Caller->>Emitter: Success or StreamPage(data, options)
  Emitter->>SafetyProvider: Scan(data)
  SafetyProvider-->>Emitter: Alert or scan result
  Emitter->>NoticeProvider: Notice()
  NoticeProvider-->>Emitter: Notice value
  Emitter->>OutputWriter: Emit envelope or formatted page
Loading

Suggested reviewers: liangshuo-1

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: introducing an output emitter contract plus a differential harness.
Description check ✅ Passed The description follows the required template with Summary, Changes, Test Plan, and Related Issues filled in.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/output-emitter-convergence

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.

@codecov

codecov Bot commented Jul 15, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.87611% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 74.70%. Comparing base (37d490a) to head (001e2f8).
⚠️ Report is 16 commits behind head on main.

Files with missing lines Patch % Lines
internal/output/emitter.go 77.87% 16 Missing and 9 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1899      +/-   ##
==========================================
+ Coverage   74.66%   74.70%   +0.03%     
==========================================
  Files         877      879       +2     
  Lines       91731    92007     +276     
==========================================
+ Hits        68494    68734     +240     
- Misses      17926    17947      +21     
- Partials     5311     5326      +15     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown

🚀 PR Preview Install Guide

🧰 CLI update

npm i -g https://pkg.pr.new/larksuite/cli/@larksuite/cli@001e2f80fad31baa607ba18d5d23cccf29700b0a

🧩 Skill update

npx skills add larksuite/cli#feat/output-emitter-convergence -y -g

@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: 2

🧹 Nitpick comments (2)
internal/output/emitter.go (2)

203-222: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate "unknown format → warn & fall back to json" logic; also silently coerces an unsupported option.

The block at Lines 212-215 is identical to StreamPage's block at Lines 130-133 — worth extracting into a shared helper (e.g., resolveFormat(errOut io.Writer, raw string) Format) to avoid drift between the two paths.

Separately, per the coding guideline for **/*.go ("never silently fall back, coerce unsupported values... Return a typed validation error when a requested option cannot be honored"), silently rewriting an unrecognized Format to JSON (rather than a typed validation error) is exactly the pattern the guideline calls out. This appears intentional here for byte-identical legacy parity (per the PR's stated goal) and the emitter isn't wired to any production caller yet, so I'm not blocking on it — but this should be revisited (return a typed error instead of silently falling back) before this path becomes the production code path.

🤖 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 `@internal/output/emitter.go` around lines 203 - 222, Extract the duplicated
unknown-format warning and JSON fallback used by emitFormatted and StreamPage
into a shared resolveFormat helper, preserving both paths’ current behavior and
warning text. Leave the unsupported-format fallback unchanged for now, but keep
the helper boundary suitable for later replacement with a typed validation
error.

Source: Coding guidelines


224-241: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

New loose-map type + in-place mutation of caller-supplied data.

emitterDataMap is a fresh loose-map type introduced solely to dodge PrintJson's type switch, and m["_notice"] = notice mutates the caller's own map in place — any code retaining a reference to data after this call will see an injected _notice key it never added. As per coding guidelines, **/*.go files should "Parse map[string]interface{} into typed structs at boundaries... Do not introduce new loose-map code."

This appears to replicate pre-existing FormatValue JSON-branch behavior for byte-parity, so I'm not asking for behavior change now — but consider copying into a local map (maps.Clone or manual copy) before injecting _notice, so the emitter never mutates caller-owned data as a side effect.

🤖 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 `@internal/output/emitter.go` around lines 224 - 241, Update
Emitter.printLegacyDataJSON to avoid mutating the caller-supplied map when
adding _notice: clone the map into a local copy before injection, while
preserving the existing JSON output and PrintJson behavior. Remove the need for
the emitterDataMap loose-map workaround if the copied data can be passed through
the established typed path.

Source: Coding guidelines

🤖 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 `@internal/output/emitter_legacy_diff_test.go`:
- Around line 434-465: The TestEmitterStreamPageMatchesPaginationLegacyOracle
cases must cover the opts.JQ != "" branch. Add a jq oracle case that supplies a
non-empty JQ expression, then validate the resulting error with errors.As and
assert the ValidationError.Param field directly; do not rely solely on
assertEquivalentError, which only compares ProblemOf.

In `@internal/output/emitter.go`:
- Around line 174-180: Update the Raw no-JQ branch in the emitter’s Success
method to return the error from enc.Encode(env) instead of discarding it, so
output write failures propagate to callers. Add coverage in
TestEmitterPropagatesOutputError for Raw: true with an empty JQ value and a
failing writer.

---

Nitpick comments:
In `@internal/output/emitter.go`:
- Around line 203-222: Extract the duplicated unknown-format warning and JSON
fallback used by emitFormatted and StreamPage into a shared resolveFormat
helper, preserving both paths’ current behavior and warning text. Leave the
unsupported-format fallback unchanged for now, but keep the helper boundary
suitable for later replacement with a typed validation error.
- Around line 224-241: Update Emitter.printLegacyDataJSON to avoid mutating the
caller-supplied map when adding _notice: clone the map into a local copy before
injection, while preserving the existing JSON output and PrintJson behavior.
Remove the need for the emitterDataMap loose-map workaround if the copied data
can be passed through the established typed path.
🪄 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: 9964bff4-3fb6-48e0-b7c2-2ebece4a3bba

📥 Commits

Reviewing files that changed from the base of the PR and between 49b4ccc and 84d1f97.

📒 Files selected for processing (2)
  • internal/output/emitter.go
  • internal/output/emitter_legacy_diff_test.go

Comment thread internal/output/emitter_legacy_diff_test.go
Comment thread internal/output/emitter.go
Introduce a leaf Emitter in internal/output that composes the existing
output primitives (content-safety scan, envelope, jq, format rendering,
notice) behind a single command-scoped port. The emitter is unwired: no
production caller is migrated, so CLI output stays byte-for-byte unchanged.

A differential test harness drives the real legacy entry points
(RuntimeContext.Out/OutRaw/OutFormat/..., WriteSuccessEnvelope and the
pagination formatter) and asserts byte-identical stdout/stderr plus typed
errors, locking behavior before later slices migrate callers.
@sang-neo03 sang-neo03 force-pushed the feat/output-emitter-convergence branch from 84d1f97 to 001e2f8 Compare July 15, 2026 07:26
@sang-neo03

Copy link
Copy Markdown
Collaborator Author

Both CodeRabbit findings are addressed in 001e2f8:

  • Critical (raw JSON path in emitter.go): the encoder error is now returned (return enc.Encode(env)) instead of discarded, so Success no longer reports success on a failed write. Locked by a new TestEmitterRawJSONPropagatesWriteError.
  • Minor (StreamPage --jq): added TestEmitterStreamPageRejectsJQ, which asserts the *errs.ValidationError and its --jq param via errors.As.

The only remaining red check is e2e-live, which is failing on upstream HTTP 429 rate-limiting of the shared test account (unrelated to this change, which adds two unwired files and touches no live API path).

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd emitter.go internal/output --exec sed -n '1,260p' {}

Length of output: 7680


Confirmed both fixes in the current emitter.go:

  • The raw-JSON envelope path now return enc.Encode(env) instead of discarding the error, so a write failure will surface as an error from Success.
  • StreamPage returns a *errs.ValidationError with --jq when opts.JQ != "", before any pagination state is touched.

Both look correctly implemented and match the description. Kicking off a full review now to check the rest of the diff, including the new test file.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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

Labels

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