Skip to content

[AK-613] Codify codebase patterns as automated lint rules #77

Description

@robinbraemer

Migrated from Linear: AK-613

GitHub is the canonical source of truth. The Linear issue is archived.


Why

Many CNAP codebase patterns are documented in .claude/rules/*.md but enforcement is human-only — code review (often AI-assisted, often incomplete). Multiple violations have shipped despite being against documented rules:

  • as never casts (banned per authorization.md) — found 8+ instances during PR #131/#132 review
  • err.name === 'WorkflowExecutionAlreadyStartedError' instead of instanceof (per temporal-workflows.md) — found 2 instances
  • ?.plan?.tier inline tier resolution (per CEP-0021) — found 6+ instances
  • paginationOptsValidator with hand-rolled return shape instead of paginationResultValidator (per convex.md) — found 1+ instance
  • .collect() for counting (per CLAUDE.md) — 117 instances codebase-wide (CNAP-611)
  • automation@cnap.tech git author email (should be @akua.dev) — found 5 instances
  • ArgoCD/Temporal/Convex name leaks in user-facing step strings — found 1 instance
  • .catch((err: unknown) => { void err; }) verbose form — found 5 instances

A linter would catch all of these at PR open time, before reaching a reviewer.

Linter setup today

  • ESLint 9 flat config (apps/web/eslint.config.js)
  • typescript-eslint strict
  • svelte plugin
  • prettier
  • oxlint (Rust-based, fast) with --type-aware mode
  • oxlint-tsgolint (newer type-aware variant)

oxlint does NOT yet support custom plugin rules (per their roadmap). Custom rules require ESLint, AST-grep, or pre-commit grep.

Phased plan

Phase 1 — Pre-commit grep (fast wins, low effort)

Add a task lint:patterns step to preflight that runs grep-based checks. Brittle but catches the obvious stuff. Each check is one regex + an allowlist.

Patterns ready to ship today (concrete regexes):

Pattern Regex Allowlist
as never outside boundary casts \bas never\b apps/web/src/lib/workflow/children.ts (documented)
err.name === 'WorkflowExecutionAlready...' err\.name\s*===\s*['"]WorkflowExecutionAlready none (always wrong)
?.plan?.tier outside billing/ \?\.plan\?\.tier apps/web/src/lib/api/server/billing/ (when CEP-0021 ships)
automation@cnap\.tech git author automation@cnap\.tech none
ArgoCD/Temporal in step names step\([^,]+,\s*['"][^'"]*\b(ArgoCD|Argo|Temporal|Convex|Karpenter)\b none
\.catch\(\(err:\s*unknown\)\s*=>\s*\{\s*void err exact pattern none

Phase 2 — Custom ESLint plugin (proper AST rules)

Create apps/web/.claude/eslint-plugin-cnap/ (or as a workspace package) exporting rules:

  • cnap/no-temporal-name-check — flag err.name === 'Workflow*' and similar; suggest instanceof
  • cnap/no-inline-tier-resolution — AST detection of ?.plan?.tier, tier === 'pro', tier === 'free' outside billing/
  • cnap/require-pagination-result-validator — when a query has paginationOpts: paginationOptsValidator AND returns: ..., require the returns to be paginationResultValidator(...)
  • cnap/no-collect-for-counting — flag .collect().length and .collect().filter(...).length patterns
  • cnap/no-as-never — flag as never outside an allowlist of files

Bigger investment but precise. Rules can ship one at a time.

Phase 3 — AST-grep (middle ground)

For patterns that are too AST-heavy for grep but don't justify a full ESLint rule. Useful for codebase-wide structural patterns like "actor.onReady that doesn't call setXActorWorkflowId".

Acceptance Criteria

  • Phase 1: task lint:patterns exists, runs in CI, fails on violations of the regex table above
  • Phase 1: documented in .claude/rules/CONTRIBUTING.md or similar so contributors understand
  • Phase 2: ESLint plugin scaffold exists with at least 3 rules ported from grep
  • Phase 2: each ported rule has a test (npm test for the plugin)
  • Existing rules files (.claude/rules/*.md) updated to reference which lint rule enforces each guideline

Out of Scope

  • Custom rules for oxlint (their plugin API isn't shipped yet — track upstream)
  • Replacing .claude/rules/*.md with code-only rules (rules carry rationale + examples that lint messages can't)
  • Phase 3 unless concrete need surfaces

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions