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
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
Why
Many CNAP codebase patterns are documented in
.claude/rules/*.mdbut enforcement is human-only — code review (often AI-assisted, often incomplete). Multiple violations have shipped despite being against documented rules:as nevercasts (banned perauthorization.md) — found 8+ instances during PR #131/#132 reviewerr.name === 'WorkflowExecutionAlreadyStartedError'instead ofinstanceof(pertemporal-workflows.md) — found 2 instances?.plan?.tierinline tier resolution (per CEP-0021) — found 6+ instancespaginationOptsValidatorwith hand-rolled return shape instead ofpaginationResultValidator(perconvex.md) — found 1+ instance.collect()for counting (perCLAUDE.md) — 117 instances codebase-wide (CNAP-611)automation@cnap.techgit author email (should be@akua.dev) — found 5 instances.catch((err: unknown) => { void err; })verbose form — found 5 instancesA linter would catch all of these at PR open time, before reaching a reviewer.
Linter setup today
apps/web/eslint.config.js)--type-awaremodeoxlint 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:patternsstep 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):
as neveroutside boundary casts\bas never\bapps/web/src/lib/workflow/children.ts(documented)err.name === 'WorkflowExecutionAlready...'err\.name\s*===\s*['"]WorkflowExecutionAlready?.plan?.tieroutside billing/\?\.plan\?\.tierapps/web/src/lib/api/server/billing/(when CEP-0021 ships)automation@cnap\.techgit authorautomation@cnap\.techstep\([^,]+,\s*['"][^'"]*\b(ArgoCD|Argo|Temporal|Convex|Karpenter)\b\.catch\(\(err:\s*unknown\)\s*=>\s*\{\s*void errPhase 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— flagerr.name === 'Workflow*'and similar; suggestinstanceofcnap/no-inline-tier-resolution— AST detection of?.plan?.tier,tier === 'pro',tier === 'free'outside billing/cnap/require-pagination-result-validator— when a query haspaginationOpts: paginationOptsValidatorANDreturns: ..., require the returns to bepaginationResultValidator(...)cnap/no-collect-for-counting— flag.collect().lengthand.collect().filter(...).lengthpatternscnap/no-as-never— flagas neveroutside an allowlist of filesBigger 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
task lint:patternsexists, runs in CI, fails on violations of the regex table above.claude/rules/CONTRIBUTING.mdor similar so contributors understandnpm testfor the plugin).claude/rules/*.md) updated to reference which lint rule enforces each guidelineOut of Scope
.claude/rules/*.mdwith code-only rules (rules carry rationale + examples that lint messages can't)References