mirror of
https://github.com/larksuite/cli.git
synced 2026-07-05 15:47:54 +08:00
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.
110 lines
3.8 KiB
Markdown
110 lines
3.8 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```go
|
|
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`:
|
|
|
|
```go
|
|
var scanners = []scanner{
|
|
{name: "errscontract", fn: errscontract.ScanRepo},
|
|
{name: "<domain>", fn: <domain>.ScanRepo}, // ← add here
|
|
}
|
|
```
|
|
|
|
5. Verify locally:
|
|
|
|
```bash
|
|
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.
|