Documentation
¶
Overview ¶
Package lint holds the `forge lint` command group — the project linter pipeline (golangci / buf / frontend / forge-convention / scaffold / migration-safety / wire-coverage …) plus the targeted single-rule flags and the --json aggregator.
It is a dir-nested command group (the devspace idiom). The substrate — the ~30 run*/collect* linter entry points and the shared linterStep table — moved here verbatim from package cli so the command is the substrate's home, not a thin shim over package-cli internals. The few genuinely cross-cutting helpers it needs (project-store loader, the forge.yaml-not-found sentinel, the user-vs-maintainer flag split) come from the leaf packages internal/cli/factory and internal/cli/cmdutil, so the group never imports internal/cli (which would cycle — internal/cli blank-imports the groups).
audit.go (now the internal/cli/audit group) reuses two of this package's collectors — CollectOptionalDepsGuardFindings and CollectConfigDepsFindings — via the exported aliases in lint_optional_deps_guard.go / lint_config_deps.go. That is a clean group→group dependency (audit → lint), no cycle.
`forge lint --generated-drift` — the gate on hand-edited generated files.
── Why a lint gate exists at all ─────────────────────────────────────
forge already DETECTS this: every Tier-1 (regenerated-every-run) file carries a `// Code generated by forge. DO NOT EDIT.` banner plus its own content hash, and generator.ScanProjectDrift reports every file whose bytes no longer match that hash. Until this step, though, the only surfaces that acted on the signal were `forge generate` (the pre-pipeline stomp guard) and `forge ci verify-generated`, which leaves a real hole:
- the stomp guard fires AT generate time, which is by definition the moment the edit is about to be destroyed. Everything between the edit and the next generate is silent.
- the guard also SCOPES its drift set to the emitters that would run this invocation (generate_tier1_scope.go). Drift in an out-of-scope file is a warning, not a failure — so an edit to, say, pkg/app/testing.go under a `--steps=…` run is waved through.
- `forge project audit` reports the same drift, but audit is a non-gating snapshot by design: it always exits 0.
So an agent (or a human) could hand-edit a DO-NOT-EDIT file, watch every check pass, commit it, and lose the work at the next regenerate. `forge lint` is the check that runs BETWEEN edits and generate — in the agent loop and in the CI lint job — so that is where the gate belongs. It scans the whole tree with no emitter-scope filter: if forge owns the bytes and the bytes changed, lint fails.
── No false positives ────────────────────────────────────────────────
The gate reports exactly what generator.ScanProjectDrift reports (see that file for the by-construction exclusions: scaffold-once files, disowned paths, files forge never wrote, pristine-older renders, formatter-only churn, Tier-2-managed starters), minus one more class handled here: the `unverified-legacy` sentinel. That marker means the one-time legacy-manifest migration could not ESTABLISH provenance — it is not evidence of an edit — so it is reported as a warning that routes the user to `forge generate`, never as a gating error.
A gate that cries wolf gets disabled, so every one of those exclusions is load-bearing.
File: internal/cli/lint/lint_pdb.go
pdb-blocks-drain — a PodDisruptionBudget that can never allow a disruption (minAvailable >= replicas, maxUnavailable 0, minAvailable 100%) blocks every node drain that touches its pods, forever. The check runs over each env's real render, so it covers forge-rendered PDBs and hand-written forge.Manifests alike. Warnings only: a render that fails here is skipped, since lint judges the project and `forge env render` owns render errors.
`forge lint --suggest-buf-excepts` walks the project's .proto files, runs `buf lint`, and prints a buf.yaml `lint.except:` snippet suggesting which STANDARD rules to silence for a port-in-progress codebase.
Migration projects (a Connect API spec ported from a pre-forge repo) routinely hit the same handful of STANDARD rules: PACKAGE_VERSION_SUFFIX when the original namespace was `foo.v1` vs the buf-canonical `foo.v1`, RPC_REQUEST_STANDARD_NAME when the RPCs predate the `<Method>Request` convention, FIELD_LOWER_SNAKE_CASE when fields were snake_case-with-acronyms, etc. The hint at the end of `forge lint` (printBufLintExceptHint) already nudges the user toward the four most common offenders, but the nudge is a static list — it can't tell which rules ACTUALLY fire in the user's tree.
This command runs `buf lint` and aggregates the output by rule name, then prints the rules that fired across more than the `--threshold` (default 3) files. The heuristic is intentionally conservative: a single .proto with one violation is probably a real bug to fix; the same violation across many files is almost always a port-wide convention mismatch best resolved via except.
The output is YAML-shaped so the user can paste it directly into buf.yaml. Nothing is mutated.
FRICTION 2026-06-02: cp-forge proto port — author hit FIELD_LOWER_SNAKE_CASE on 40+ files and had to grep buf output manually to count rule occurrences before deciding what to except.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ErrProjectConfigNotFound = cmdutil.ErrProjectConfigNotFound
ErrProjectConfigNotFound is the canonical "forge.yaml not found" sentinel, re-exported from cmdutil so the lint helpers compare against the same value internal/cli's config.ErrProjectConfigNotFound aliases.
Functions ¶
func SetGateWriter ¶ added in v0.1.45
func SetGateWriter(w GateWriter)
SetGateWriter installs the gate writer. Called once, from the CLI package.
func WriteGateForTest ¶ added in v0.1.45
func WriteGateForTest(req GateRequest) error
WriteGateForTest exposes the installed gate writer so the CLI package can assert that --gate-json is actually wired. A test-only seam in production code, deliberately: the wiring happens in an init() in the CLI package, and nothing else can observe whether it ran.
Types ¶
type ConfigDepsFinding ¶
type ConfigDepsFinding = configDepsFinding
ConfigDepsFinding is the exported alias for the config-deps finding, consumed by the internal/cli/audit group's auditConfigDeps roll-up. A type alias (not a new type) so audit's field access (.Role/.Package/.File/.Line/.Field/.Type) is unchanged from when both commands lived in package cli.
func CollectConfigDepsFindings ¶
func CollectConfigDepsFindings(projectDir string) ([]ConfigDepsFinding, error)
CollectConfigDepsFindings is the exported entry the audit group calls. It forwards to the package-internal collector; the audit roll-up is the only cross-package consumer.
type GateRequest ¶ added in v0.1.45
type GateRequest struct {
// Path is where the document is written.
Path string
// Name is the check: "lint".
Name string
// Status is the verdict.
Status string
// Summary is the one line a reader sees.
Summary string
// Details is small and structured — counts, never logs.
Details map[string]any
// StartedAt and FinishedAt are the CHECK's own window, which is what
// makes a gate a run stage with a duration.
StartedAt time.Time
FinishedAt time.Time
}
GateRequest is one completed check, as the CLI needs it to write a gate document. A struct rather than six parameters because it crosses a package boundary and every field is optional-ish detail about the same event.
type GateWriter ¶ added in v0.1.45
type GateWriter func(req GateRequest) error
GateWriter writes a gate document.
Declared HERE, at the consumer, rather than exported from the CLI: this package needs "something that can write a gate", and that one-method contract is all it is told about the gate vocabulary.
type OptionalDepsGuardFinding ¶
type OptionalDepsGuardFinding = optionalDepsGuardFinding
OptionalDepsGuardFinding is the exported alias for the optional-deps-guard finding, consumed by the internal/cli/audit group's auditOptionalDepsGuard roll-up. A type alias (not a new type) so audit's field access (.Role/.Package/.File/.Line/.Expr/.Method) is unchanged from when both commands lived in package cli.
func CollectOptionalDepsGuardFindings ¶
func CollectOptionalDepsGuardFindings(projectDir string) ([]OptionalDepsGuardFinding, error)
CollectOptionalDepsGuardFindings is the exported entry the audit group calls. It forwards to the package-internal collector; the audit roll-up is the only cross-package consumer.
Source Files
¶
- bootstrap.go
- contract_inprocess.go
- enforce_component_observe.go
- export_test_gate.go
- golangci_toolchain.go
- lint.go
- lint_column_markers.go
- lint_commit_policy.go
- lint_computed_fields.go
- lint_config_deps.go
- lint_config_reach.go
- lint_create_nullability.go
- lint_fixture_drift.go
- lint_format.go
- lint_format_frontend.go
- lint_frontend_typecheck.go
- lint_gate.go
- lint_generated_drift.go
- lint_guarded_fields.go
- lint_hosted_image_base.go
- lint_json.go
- lint_migration_sql.go
- lint_no_dotenv.go
- lint_optional_deps_guard.go
- lint_pdb.go
- lint_pending_stubs.go
- lint_proto_markers.go
- lint_proto_options.go
- lint_read_only_fields.go
- lint_shellbuild_tokens.go
- lint_show_suppressed.go
- lint_static_export.go
- lint_steps.go
- lint_structured.go
- lint_suggest_buf_excepts.go
- lint_suggest_excludes.go
- lint_time_bucketing.go
- lint_typed_access_guard.go
- lint_unwritten_outcome.go
- lint_vendored_protos.go
- lint_workload_drift.go