Skip to content

feat(doc): add --preview flag to docs +update for pre-write self-check#624

Open
herbertliu wants to merge 2 commits intomainfrom
feat/docs-update-preview
Open

feat(doc): add --preview flag to docs +update for pre-write self-check#624
herbertliu wants to merge 2 commits intomainfrom
feat/docs-update-preview

Conversation

@herbertliu
Copy link
Copy Markdown
Collaborator

@herbertliu herbertliu commented Apr 23, 2026

Summary

Addresses Case 11 in the lark-cli pitfall list: the existing `--dry-run` on `docs +update` only prints the request body, so Agents can verify the RPC shape but have no idea whether their selection actually resolves in the target doc. The result is blind writes followed by fetch-doc forensics.

Add `--preview` as a content-aware alternative: pre-fetch the document, locate the selection in its markdown, emit a structured result describing match positions, the raw payload, and human-readable notes for common traps.

Changes

  • `shortcuts/doc/docs_update_preview.go` (new): `previewAtxHeadingLevel` (CommonMark-conformant), `findSelectionWithEllipsis` / `findSelectionByTitle` / `findSingleLineMatches` helpers, `buildPreviewResult`, `fetchMarkdownForPreview`.
  • `shortcuts/doc/docs_update.go`: new `--preview` bool flag; Execute short-circuits to the preview path when set.
  • `shortcuts/doc/docs_update_preview_test.go` (new): 12-case ATX helper + 4-subtest ellipsis matcher + 4-subtest title matcher + 5-subtest preview-result builder.

Shape

```bash
lark-cli docs +update --doc --mode replace_range \
--selection-with-ellipsis "old intro...ending here" \
--markdown "new intro" --preview
```

```json
{
"mode": "replace_range",
"doc_id": "",
"match_count": 1,
"matches": [
{"StartLine": 7, "EndLine": 8, "Snippet": "old intro\nending here"}
],
"payload_markdown": "new intro",
"payload_lines": 1,
"note": "selection matches exactly one location"
}
```

Scope notes

  • Local-only matcher: preview operates on the raw fetched markdown, not on the server's block tree. A miss is advisory — the server may still resolve the selection differently. This is documented on every matcher and on the flag's description. Preview is for Agent self-check, not for blocking the write path.
  • `append` / `overwrite`: selection lookup is skipped; the note instead describes what the mode would do (append N lines / discard entire doc).
  • `previewAtxHeadingLevel` is a CommonMark-conformant ATX parser local to this file, distinct from the one in PR feat(doc): warn when replace_range targets a heading but markdown lacks matching heading #619 (Case 2). The two will converge once both land on main.
  • The rendered-after view is deliberately not attempted — CLI doesn't know how the server will convert markdown into Lark blocks, so simulating it would mislead.

Test Plan

  • `go test ./shortcuts/doc/...` passes
  • `go vet ./...` clean
  • `gofmt -l` clean
  • Manual smoke: run `--preview` against a live doc with a replace_range + unique selection and confirm the match + note

Summary by CodeRabbit

  • New Features

    • Added a --preview flag to docs +update so users can see a structured preview of proposed changes (matched locations, computed line counts, and outgoing payload) without modifying the document. Preview supports append/overwrite semantics and clear human-readable notes for 0/1/multiple matches.
  • Tests

    • Added end-to-end tests covering heading detection, selection matching, multi-match behavior, append/overwrite handling, and preview result generation.

Case 11 in the pitfall list: docs +update's existing --dry-run prints
the request body, which lets Agents verify the RPC shape but says
nothing about whether their selection actually resolves in the target
doc or where in the doc it lands. The gap shows up as Agents running
blind writes, finding they hit the wrong block, and then reverse-
engineering via fetch-doc.

Add --preview as a content-aware alternative: pre-fetch the document,
locate the selection in its markdown, and emit a structured result
describing match positions (1-based line ranges), the raw markdown
payload that would be sent, and a human-readable note flagging common
traps (no match, multi-match under a mode that acts on first match,
overwrite discarding everything). Execute short-circuits before the
write — the response is purely advisory.

append / overwrite skip selection lookup since neither anchors against
existing content.

The preview operates on the raw fetched markdown, not on the server's
block tree, so a miss here is advisory (server may resolve the
selection differently). This tradeoff is documented on the matcher
helpers and in the --preview flag description.

Coverage: 12-case ATX helper table, 4-subtest ellipsis matcher, 4-
subtest title matcher, 5-subtest preview-result builder.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 23, 2026

📝 Walkthrough

Walkthrough

Adds a --preview mode to docs update that fetches the current markdown, locally matches the requested selection (ellipsis or title-based section), builds a structured preview payload describing the would-be edit, and exits without performing the MCP update-doc call.

Changes

Cohort / File(s) Summary
Command Flag
shortcuts/doc/docs_update.go
Adds a --preview boolean flag and a preview execution branch to the DocsUpdate command.
Preview Implementation
shortcuts/doc/docs_update_preview.go
New preview module: fetches markdown snapshot, infers ATX heading levels, matches selections by ellipsis or heading title (with fenced-code skipping), and constructs a deterministic previewResult payload (match ranges, snippets, notes, payload line counts). No write path invoked.
Tests
shortcuts/doc/docs_update_preview_test.go
New test suite covering ATX heading detection, ellipsis and title-based selection matching (including edge cases), fenced-code handling, and buildPreviewResult behaviors for append/overwrite/replace variants and match-count notes.

Sequence Diagram

sequenceDiagram
    participant User as User/CLI
    participant Cmd as docs update (--preview)
    participant MCP as MCP: fetch-doc
    participant Parser as Markdown Parser
    participant Matcher as Selection Matcher
    participant Preview as Preview Builder

    User->>Cmd: run docs update --preview
    Cmd->>MCP: fetch current document markdown
    MCP-->>Cmd: return markdown snapshot
    Cmd->>Parser: parse markdown, infer headings
    Parser-->>Cmd: heading metadata
    Cmd->>Matcher: match selection (ellipsis or title)
    Matcher-->>Cmd: match ranges & snippets
    Cmd->>Preview: build previewResult (payload, counts, notes)
    Preview-->>Cmd: preview object
    Cmd-->>User: emit preview output and exit (no update)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Suggested reviewers

  • fangshuyu-768
  • SunPeiYang996

Poem

🐰 In quiet burrows I nudge the draft,
I peek at headings, each section and shaft.
No pen yet pressed, just a careful preview,
Snippets and notes—I'll show them to you.
Hop lightly, update when the time is true. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 53.85% 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 accurately summarizes the main change: adding a --preview flag to docs +update command for pre-write verification of selections.
Description check ✅ Passed The description includes Summary, Changes, and Test Plan sections matching the template, with comprehensive details on motivation, implementation, and test coverage.
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/docs-update-preview

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.

@github-actions github-actions Bot added domain/ccm PR touches the ccm domain size/M Single-domain feat or fix with limited business impact labels Apr 23, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 23, 2026

Codecov Report

❌ Patch coverage is 79.66102% with 36 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.28%. Comparing base (81d22c6) to head (ee53d9d).

Files with missing lines Patch % Lines
shortcuts/doc/docs_update.go 0.00% 19 Missing ⚠️
shortcuts/doc/docs_update_preview.go 89.24% 15 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #624      +/-   ##
==========================================
+ Coverage   60.20%   60.28%   +0.08%     
==========================================
  Files         407      408       +1     
  Lines       43340    43514     +174     
==========================================
+ Hits        26091    26232     +141     
- Misses      15221    15252      +31     
- Partials     2028     2030       +2     

☔ View full report in Codecov by Sentry.
📢 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
Copy link
Copy Markdown

github-actions Bot commented Apr 23, 2026

🚀 PR Preview Install Guide

🧰 CLI update

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

🧩 Skill update

npx skills add larksuite/cli#feat/docs-update-preview -y -g

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shortcuts/doc/docs_update_preview.go`:
- Around line 119-132: The current loop that builds previewMatch entries
(iterating over lines with indices i and j and appending previewMatch with
StartLine, EndLine and Snippet) can produce false positives when start and end
are on the same line but end appears before start; modify the inner loop so that
before appending a previewMatch you check if i == j and, in that case, compute
the positions of start and end in lines[i] (using strings.Index/LastIndex or
equivalent) and only append if the start position is <= end position (i.e.,
start occurs before end); keep existing behavior for multi-line matches and skip
adding the match when the same-line ordering check fails.
- Around line 221-228: Add a branch to set res.Note when res.MatchCount > 1 and
mode == "replace_all" so the preview includes an advisory for multi-match
replace_all operations; locate the switch that sets res.MatchCount and Note (the
block referencing res.Matches, res.MatchCount and the variable mode) and add a
case for res.MatchCount > 1 && mode == "replace_all" that assigns a helpful
message (similar tone to the existing multi-match message) describing that
replace_all will apply to all matches.
- Around line 166-182: The heading scanner must ignore fenced code blocks: add a
boolean (e.g., inFence) and toggle it when a trimmed line starts with "```" or
"~~~", and if inFence is true skip processing the line so previewAtxHeadingLevel
won't treat fenced-heading-like lines as real headings; apply this fence-check
both in the outer loop that finds the target heading (the for i, line := range
lines block that calls previewAtxHeadingLevel and compares to
targetLevel/targetText) and in the inner section-end scan (the for j := i+1 loop
that sets end) so fenced headings neither match the target nor terminate the
section.

In `@shortcuts/doc/docs_update.go`:
- Around line 108-116: The preview path drops the new title so preview output
can hide title mutations; update the preview result to include the same
new_title field that the real write path sends by extending buildPreviewResult
and the previewResult shape to accept and emit new_title (same value used at
write time), then pass that new_title when constructing preview in the preview
variable before calling runtime.Out(preview, nil) so `--preview --new-title`
reflects the pending title change.
🪄 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: 927f7da3-fe17-4012-a356-f28ba4c4fb08

📥 Commits

Reviewing files that changed from the base of the PR and between 81d22c6 and f0fdd82.

📒 Files selected for processing (3)
  • shortcuts/doc/docs_update.go
  • shortcuts/doc/docs_update_preview.go
  • shortcuts/doc/docs_update_preview_test.go

Comment thread shortcuts/doc/docs_update_preview.go
Comment thread shortcuts/doc/docs_update_preview.go
Comment thread shortcuts/doc/docs_update_preview.go
Comment thread shortcuts/doc/docs_update.go
…dings, replace_all note, new_title

CodeRabbit review on the preview matcher flagged four edges:

1. findSelectionWithEllipsis on a same-line selection like 'A...B' was
   accepting lines where B appeared before A, because the scan only
   checked substring presence not relative position. Track the start
   offset and require end to appear after it within the same line.

2. findSelectionByTitle treated ATX-heading-looking lines inside
   '```' or '~~~' fences as real headings, producing false matches
   (for '## Example' pasted into a code sample) and incorrect section
   boundaries (fenced headings terminating outer sections). Pre-compute
   in-fence state per line and skip those rows in both the outer match
   scan and the inner section-end scan.

3. replace_all with multiple matches left Note empty — exactly the mode
   where the count is most actionable. Emit 'selection matches N
   locations; replace_all would apply to all matches'.

4. --new-title was silently dropped in preview output, so a user
   running --preview --new-title could not see the pending title
   change. Thread it through buildPreviewResult and surface it as
   previewResult.NewTitle in the JSON response.

Coverage: three new tests pin the fixes — same-line reversed ordering,
fenced heading false-positive, replace_all multi-match note — plus the
existing table is extended with a new-title propagation assertion.
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@shortcuts/doc/docs_update_preview.go`:
- Around line 108-112: The no-ellipsis branch in findSelectionWithEllipsis only
calls findSingleLineMatches so a multi-line selection (contains '\n' but not
'...') is never matched; update findSelectionWithEllipsis to detect when
selection contains a newline and, in that case, search the whole markdown for an
exact substring match (e.g., using strings.Contains/Index) and return the
corresponding previewMatch(s) instead of deferring to findSingleLineMatches;
apply the same fix to the analogous logic around the code at the 148-161 region
and add a regression test that supplies a selection with newline(s) but no "..."
to ensure the exact multi-line match is returned.
- Around line 55-64: fetchMarkdownForPreview currently downcasts
result["markdown"] to string and returns an empty string on failure; instead
detect when the "markdown" key is missing or not a string and return a clear
error so callers can fail the preview. In fetchMarkdownForPreview (calling
common.CallMCPTool), check the type assertion result["markdown"].(string) with
the comma-ok idiom, and if ok is false or the string is empty return a
descriptive error (e.g., fmt.Errorf("fetch-doc returned no markdown for doc %s",
docID)) rather than returning "", nil.
🪄 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: 360672a0-56df-441e-8b61-e894ca2765de

📥 Commits

Reviewing files that changed from the base of the PR and between f0fdd82 and ee53d9d.

📒 Files selected for processing (3)
  • shortcuts/doc/docs_update.go
  • shortcuts/doc/docs_update_preview.go
  • shortcuts/doc/docs_update_preview_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • shortcuts/doc/docs_update_preview_test.go
  • shortcuts/doc/docs_update.go

Comment on lines +55 to +64
func fetchMarkdownForPreview(runtime *common.RuntimeContext, docID string) (string, error) {
result, err := common.CallMCPTool(runtime, "fetch-doc", map[string]interface{}{
"doc_id": docID,
"skip_task_detail": true,
})
if err != nil {
return "", err
}
md, _ := result["markdown"].(string)
return md, nil
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

Fail preview when fetch-doc does not return markdown.

Line 63 silently treats a missing/non-string markdown field as an empty document, which can produce a misleading “no match” or “0 lines” preview for a malformed response.

Proposed fix
 	md, _ := result["markdown"].(string)
+	if _, ok := result["markdown"]; !ok {
+		return "", fmt.Errorf("fetch-doc response missing markdown field")
+	}
+	md, ok := result["markdown"].(string)
+	if !ok {
+		return "", fmt.Errorf("fetch-doc response markdown field is not a string")
+	}
 	return md, nil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/doc/docs_update_preview.go` around lines 55 - 64,
fetchMarkdownForPreview currently downcasts result["markdown"] to string and
returns an empty string on failure; instead detect when the "markdown" key is
missing or not a string and return a clear error so callers can fail the
preview. In fetchMarkdownForPreview (calling common.CallMCPTool), check the type
assertion result["markdown"].(string) with the comma-ok idiom, and if ok is
false or the string is empty return a descriptive error (e.g.,
fmt.Errorf("fetch-doc returned no markdown for doc %s", docID)) rather than
returning "", nil.

Comment on lines +108 to +112
func findSelectionWithEllipsis(markdown, selection string) []previewMatch {
lines := strings.Split(markdown, "\n")
if !strings.Contains(selection, "...") {
return findSingleLineMatches(lines, selection)
}
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

Handle exact multi-line selections without ellipsis.

The no-ellipsis path only scans individual lines, so a valid exact selection like first line\nsecond line always reports no match even when it exists in the fetched markdown.

Suggested direction
 	lines := strings.Split(markdown, "\n")
 	if !strings.Contains(selection, "...") {
+		if strings.Contains(selection, "\n") {
+			return findMultiLineExactMatches(markdown, selection)
+		}
 		return findSingleLineMatches(lines, selection)
 	}

Add a regression test with a newline-containing selectionWithEllipsis value that does not include ....

Also applies to: 148-161

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shortcuts/doc/docs_update_preview.go` around lines 108 - 112, The no-ellipsis
branch in findSelectionWithEllipsis only calls findSingleLineMatches so a
multi-line selection (contains '\n' but not '...') is never matched; update
findSelectionWithEllipsis to detect when selection contains a newline and, in
that case, search the whole markdown for an exact substring match (e.g., using
strings.Contains/Index) and return the corresponding previewMatch(s) instead of
deferring to findSingleLineMatches; apply the same fix to the analogous logic
around the code at the 148-161 region and add a regression test that supplies a
selection with newline(s) but no "..." to ensure the exact multi-line match is
returned.

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

Labels

domain/ccm PR touches the ccm domain size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant