Files
larksuite-cli/lint/README.md
evandance fe72e41fb2 feat(errs): add structured CLI error contract (#984)
Introduce a typed error contract framework for lark-cli so in-process
Go callers can branch via errors.As(&errs.XxxError{}) and shell scripts,
AI agents, and protocol adapters can branch on stable JSON type/subtype
fields instead of regex-parsing free-form messages.

Adds:
- Canonical taxonomy under errs/ (9 categories + typed Error structs
  embedding a shared Problem, RFC 7807-aligned)
- Centralized Lark code metadata + identity-aware BuildAPIError dispatch
- Typed JSON envelope writer alongside the legacy envelope writer
- MCP / OAuth (RFC 6750 Bearer) projection adapters
- Five CI lint guards preventing ad-hoc taxonomy drift

Backward compatibility: legacy *output.ExitError producers (ErrAPI,
ErrWithHint, Errorf, ErrBare) and business shortcuts that use them
continue to render the legacy envelope unchanged. SecurityPolicyError
wire format and exit code are preserved via a carve-out; taxonomy
migration is deferred to PR 2. Domain-specific business migration is
staged across PR 3+.

Framework-direct paths now return typed *errs.*Error: ErrAuth /
ErrValidation / ErrNetwork emit category literals on the wire
(authentication / validation / network), *core.ConfigError is promoted
at the cmd/root boundary with exit code aligned from 2 to 3, and Lark
API permission denials classified by BuildAPIError exit 3.

At the SDK boundary, WrapDoAPIError preserves any already-classified
error (legacy *output.ExitError or typed *errs.*) so output.ErrAuth
from missing credentials surfaces with the auth category and exit 3
intact instead of being downgraded to a network error. Policy responses
classified by BuildAPIError (codes 21000 / 21001) extract challenge_url
and the canonical hint from the response body, matching what the
auth transport already surfaces at the HTTP layer; non-https
challenge URLs are dropped.

First PR in the feat/error-contract-* series.
2026-05-26 11:42:33 +08:00

3.8 KiB

lint/

Source-level static checks that guard lark-cli conventions golangci-lint cannot express. Each lint domain is a sibling Go package under lint/; the top-level lint/main.go aggregates results and emits a single exit code.

lint/ is its own Go module so its golang.org/x/tools/go/packages dependency does not leak into the shipped lark-cli binary's module graph.

Layout

lint/
├── go.mod              # module github.com/larksuite/cli/lint
├── go.sum
├── main.go             # package main — dispatches to every registered domain
├── lintapi/            # shared types every domain returns
│   └── violation.go    # Violation, Action, ActionReject / ActionLabel / ActionWarning
└── errscontract/       # first domain: typed-error contract guards
    ├── scan.go         # ScanRepo(root) ([]lintapi.Violation, error)  ← public entry
    ├── runner.go
    ├── typecheck.go
    ├── violation.go    # local type aliases to lintapi
    ├── rule_problem_embed.go
    ├── rule_no_registrar.go
    ├── rule_adhoc_subtype.go
    ├── rule_declared_subtype.go
    ├── rule_subtype_classifier.go
    ├── rule_typed_error_completeness.go
    └── *_test.go

Running

# from the repo root (one level above lint/)
go run -C lint . ..

-C lint switches Go's working directory to lint/; the .. argument is the repo root to scan (relative to lint/).

CI: .github/workflows/ci.yml step Run errs/ lint guards (lintcheck).

Exit codes follow lint/main.go:

Code Meaning
0 no REJECT diagnostics (LABEL / WARNING are advisory)
1 one or more REJECT diagnostics
2 a domain's ScanRepo returned an error

Adding a new lint domain

  1. Create a sibling package: lint/<domain>/. Pick a name that reads like a category, not a list of rules (errscontract/ covers many error-contract rules; flagnaming/ would cover many flag-related rules).

  2. Inside the new package, expose one public entry:

    package <domain>
    
    import "github.com/larksuite/cli/lint/lintapi"
    
    // ScanRepo walks root and returns every violation produced by this
    // domain's checks. Domains MUST return []lintapi.Violation so the
    // top-level dispatcher can aggregate uniformly.
    func ScanRepo(root string) ([]lintapi.Violation, error) { ... }
    
  3. Per-rule files are named rule_<name>.go with sibling rule_<name>_test.go. Each rule function returns []lintapi.Violation. runner.go (or scan.go) composes the rules.

  4. Register the domain in lint/main.go:

    var scanners = []scanner{
        {name: "errscontract", fn: errscontract.ScanRepo},
        {name: "<domain>",     fn: <domain>.ScanRepo},  // ← add here
    }
    
  5. Verify locally:

    go test  -C lint ./...      # all domains' tests
    go run   -C lint . ..       # full scan against the repo
    
  6. Document the rules. If they enforce a contract that already has a spec (e.g. errs/ERROR_CONTRACT.md), add the lint entry to that contract's "CI guards" table. Otherwise create a short spec alongside the package.

Rule severity conventions (lintapi.Action)

Action Effect When to use
ActionReject exit 1, fails CI a contract violation that must be fixed before merge
ActionLabel stderr only; CI can grep for [needs-taxonomy-decision] and label the PR governance signal that asks a human to choose (e.g. ad_hoc_* subtype needs a taxonomy decision)
ActionWarning stderr only advisory hint surfaced to reviewers (typed scope unavailable, fallback to AST-only, etc.) — never gates merges

Only ActionReject contributes to a nonzero exit code; ActionLabel and ActionWarning are reviewer signal only.