@stryder-works/coding-standards
v0.4.1
Published
Shared coding standards lint kit for Stryder-Works
Readme
@stryder-works/coding-standards
Shared coding standards lint kit for Stryder-Works.
This repo is the source of truth for org-wide lint scripts, general coding standards, and shared testing principles. App repos and bootstrap templates consume the published npm package — they must not copy the scripts or docs into their own trees.
Do not open pull requests against other repos from work in this repo.
What lives here
scripts/— eight shared lint checks,lint-driftorchestrator, andlint-staged-utils.mjswith Vitest allow/deny testseslint/— recommended ESLint flat config for Easy rulesCODING_STANDARDS.md— pruned general coding standards for the orgdocs/agents/testing-principles.md— shared testing principles for the orgdocs/agents/issue-tracker.md— GitHub Issues ops and three-axis modeldocs/agents/label-flow.md—stage:*lane sequencedocs/agents/triage-labels.md— attention flags, closing, flake ledger.cursor/skills/issue-integration— land named issues with review loopsAGENTS.md— short agent entry pointCHANGELOG.md— Keep a Changelog history for published package versions
Pre-commit hook (this repo)
Same enforcement model as app repos: ESLint on staged files, then typecheck and
test. Run pnpm run lint:drift periodically for the full CLI proof suite.
pnpm exec lint-staged
pnpm run typecheck
pnpm run testLocal development
pnpm install
pnpm test
pnpm run lint:drift # full CLI proof suite (--tree); app repos run this periodicallypnpm run prepare installs Husky hooks when this checkout is the git toplevel.
Publishing to npm
Package name: @stryder-works/coding-standards (public scoped package).
CI stages the package from the release branch when the package version is
not yet on npm and CHANGELOG.md has a heading for that version. The
workflow is .github/workflows/publish.yml. It authenticates with npm
Trusted Publishing (OIDC). It does not use a long-lived NPM_TOKEN. The
publish step runs npm stage publish. A maintainer then approves the staged
package.
Release branch
Use the release branch for version and publish work. Do not bump
package.json version on main. The workflow file must exist on release.
Before each release, update release from main:
git checkout release
git merge mainBump the version on release (npm version patch or npm version minor,
or edit package.json). Add a Keep a Changelog section for that version in
CHANGELOG.md in the same change. Use patch for backward-compatible
fixes. Use minor for new exports, new presets, or stricter changes to
the shared ./eslint preset. publishConfig.access is already set to
public.
Push the version commit to release. The workflow then runs pnpm test and
npm stage publish when npm does not have that version.
The workflow does not require a version change vs the previous commit. It
stages when npm view reports a clear miss for the current version. After
that version is live on npm, later pushes skip. They do not stage again.
If this commit changes package.json version and npm already has that
version, the job fails.
If CHANGELOG.md has no ## [x.y.z] heading for the current version, the
job fails and does not stage. pnpm test fails for the same absent heading
before you push.
After a maintainer approves the staged package, merge release back to
main. Push the version commit and tag:
git checkout main
git merge release
git push && git push --tagsOne-time npm Trusted Publisher (Justin)
Do this once on npmjs.com before the first CI stage. The workflow cannot authenticate until this exists.
- Open package settings for
@stryder-works/coding-standards. - Add a GitHub Actions trusted publisher.
- Set organization or user to
stryder-works. - Set repository to
coding-standards(GitHub repoStryder-Works/coding-standards). - Set workflow filename to
publish.yml(filename only, with extension). - Leave allowed actions at
npm stage publish. New publishers use that default. Do not addnpm publish.
Leave the GitHub environment name empty. This workflow does not use an environment.
Docs: Trusted publishing
Approve a staged package
CI does not make the version live. After the workflow stages the package, a maintainer approves it on npmjs.com under Staged Packages, or with:
npm stage list
npm stage approve <stage-id>Approval requires 2FA. Inspect a staged tarball before you approve:
npm stage view <stage-id>
npm stage download <stage-id>Requires npm CLI 11.15.0+ and Node 22.14.0+ for npm stage.
Docs: Staged publishing
Local dry-run
From a clean checkout on release:
pnpm test
npm publish --dry-runDo not run npm publish or npm stage publish from a laptop for a normal
release. CI stages from release.
You can run the workflow by hand with workflow_dispatch. A dry run still
obeys the publish gate for a real stage.
Published package: https://www.npmjs.com/package/@stryder-works/coding-standards
License
This package uses the MIT license. Copyright (c) 2026 Justin Elias. Read LICENSE.
Consumption in app repos
Every app that pulls this package must follow this pattern. Do not copy scripts or docs from this repo into the app tree.
Declare in devDependencies
Add @stryder-works/coding-standards under devDependencies:
"devDependencies": {
"@stryder-works/coding-standards": "^0.3.0"
}pnpm:
pnpm add -D @stryder-works/coding-standardsnpm:
npm install --save-dev @stryder-works/coding-standardsBootstrap templates should wire this dependency, the shared ESLint config, and the enforcement model below for every new repo.
Enforcement model
App repos use two tiers:
Pre-commit (daily) — run the shared ESLint presets through lint-staged for
fast feedback on staged files. Do not run all eight CLI bins on every
commit.
Drift audit (periodic) — run the full CLI proof suite against the worktree
to catch ESLint gaps, legacy violations, and standards drift. Treat this like
architecture reviews or periodic useEffect audits: weekly, before major
releases, or when onboarding a repo.
pnpm exec lint-driftlint-drift runs all eight lint-* bins in --tree mode (full tracked
worktree scan). lint-comments and lint-local-vi-mock are CLI-only (no ESLint
rule). Folded rules use ESLint day-to-day; drift catches remaining gaps
documented in the tradeoff sections below (for example barrel own-repo
re-exports and describe ↔ test.describe nesting).
Deploy install (Vercel)
Some app repos skip dev dependencies on Vercel to speed deploys that do not run
lint (for example pnpm install --frozen-lockfile --prod, or
--no-optional when optional deps are also unused). That is an app-level build
choice. It is not required for @stryder-works/coding-standards — the
package is on public npm and installs with a normal pnpm install.
Cloud agents (Cursor)
The package is on the public npm registry, so a normal pnpm install installs
it in Cloud Agent VMs. No git clone token or extra repo access is required.
If pre-commit runs ESLint via lint-staged, fail the install hook when the
package did not install (for example test that
node_modules/@stryder-works/coding-standards/package.json or
node_modules/.bin/lint-drift exists — do not use
pnpm exec lint-drift --help; the lint scripts exit non-zero for --help
even when installed).
Local install
A normal local pnpm install or npm install installs dev dependencies, so
pre-commit hooks keep working.
App config (.codingstandardsrc)
Add a JSON file at the app repo root to skip paths from the shared CLI lint
scripts (lint-comments, lint-empty-catch, and the other six bins). ESLint
ignores stay in the app eslint.config (see below).
{
"ignore": ["legacy/**", "vendor/**", "generated/**"]
}Patterns are minimatch globs relative to the repo root. Omit ignore or use
an empty array when every tracked path should stay in scope.
The file is optional. Missing file means no extra ignores.
Shared ESLint config
Use this config in app pre-commit via lint-staged. Run
pnpm exec lint-drift on a schedule for the full CLI proof suite.
This package exports a recommended ESLint flat config:
@stryder-works/coding-standards/eslintThe base export spreads @eslint/js recommended, so apps do not re-declare
those core rules.
Install these peer dependencies in the app repo:
| Peer | Version range | Role |
| --- | --- | --- |
| @eslint/js | ^9.0.0 \|\| ^10.0.0 | Core recommended rules folded into the base export |
| eslint | ^9.0.0 \|\| ^10.0.0 | Core linter |
| typescript-eslint | ^8.0.0 | TypeScript parser and type-assertion rule |
| @vitest/eslint-plugin | ^1.0.0 | Vitest rules (no-focused-tests, no-hooks, max-nested-describe) |
| eslint-plugin-react-hooks (optional) | ^6.1.0 \|\| ^7.0.0 | React Hooks rules (React preset) |
| eslint-plugin-jsx-a11y (optional) | ^6.10.0 | JSX accessibility rules (React preset) |
| eslint-plugin-boundaries (optional) | ^5.0.0 | App-owned element graphs and boundaries/* rules |
Install eslint, @eslint/js, typescript-eslint, and @vitest/eslint-plugin
with this package. Install eslint-plugin-react-hooks and
eslint-plugin-jsx-a11y only when the app uses the React preset. Install
eslint-plugin-boundaries only when the app uses boundary lint.
Compose the shared config into the app eslint.config:
import codingStandards from "@stryder-works/coding-standards/eslint";
export default [...codingStandards];Add app-local configs after the spread when you need overrides.
Type-checked preset (opt-in)
For type-aware rules, spread the optional preset after the base config:
@stryder-works/coding-standards/eslint/type-checkedimport codingStandards from "@stryder-works/coding-standards/eslint";
import typeChecked from "@stryder-works/coding-standards/eslint/type-checked";
export default [
...codingStandards,
...typeChecked,
{
languageOptions: {
parserOptions: {
projectService: true,
},
},
},
];The app must supply type information (projectService or project plus
tsconfigRootDir). The preset spreads typescript-eslint
recommendedTypeChecked, sets @typescript-eslint/no-deprecated to error,
and ignores unused names that start with _.
Empty catch (no-empty)
The base export includes @eslint/js recommended, which sets core no-empty
to error. That covers empty catch blocks for ESLint consumers.
Tradeoff versus scripts/lint-empty-catch.mjs:
- Broader:
no-emptyalso flags emptyif,while,switch,try, andfinallyblocks. - Miss: a catch that holds only a comment passes
no-empty. The CLI still flags that catch (statements.length === 0).
Prefer a miss over a false positive. pnpm exec lint-drift catches gaps ESLint
misses.
Type assertions (consistent-type-assertions)
The shared config sets
@typescript-eslint/consistent-type-assertions to error with
assertionStyle: "never". That bans as and angle-bracket assertions.
It still allows as const.
Tradeoff versus scripts/lint-as-assertions.mjs:
- Match:
as Tand<T>valuein.tsfiles fail.as constpasses. - Match: real JSX in a
.tsxfile is not an assertion. - Match: files named
*.gen.ts(and the other*.gen.*TypeScript extensions) stay out of this rule, like the CLI name skip. - Miss: a file that has only a generated-marker comment (and no
.gen.name) still receives the ESLint rule. The CLI skips that file. - Miss:
<T>valuein a.tsxfile is a parse error under the TypeScript JSX parse, not an assertion finding. The CLI also skips that form.
Prefer a miss over a false positive. pnpm exec lint-drift catches gaps ESLint
misses.
Return types (explicit-function-return-type)
The shared config sets @typescript-eslint/explicit-function-return-type to
error with allowExpressions, allowTypedFunctionExpressions, and
allowHigherOrderFunctions enabled. That requires an explicit return type on
top-level function declarations while still allowing typed arrow exports,
typed function expressions, and higher-order callbacks to stay inferred.
Tradeoff versus CODING_STANDARDS.md alone:
- Match:
export function foo() { … }without a return type fails. - Match:
export function foo(): T { … }and annotated module exports pass. - Match: arrow exports with an explicit return type or a typed const annotation, typed function expressions, and higher-order wrappers pass under the allow
- Match: files named
*.gen.ts(and the other*.gen.*TypeScript extensions) stay out of this rule, like the type-assertion block. - Miss: the rule runs only on TypeScript file extensions. Plain
.js/.mjstop-level functions are not checked. - Miss: untyped exported arrow functions (
export const fn = () => …) still fail even withallowExpressions; annotate the return type or add a typed const annotation soallowTypedFunctionExpressionsapplies.
Framework-inferred returns (for example TanStack Router getRouter) stay
documented in CODING_STANDARDS.md with an inline
eslint-disable-next-line and a reason.
Prefer a miss over a false positive.
Focused tests (vitest/no-focused-tests)
The shared config sets vitest/no-focused-tests to error for test and
spec files (*.{test,spec}.*).
Tradeoff versus scripts/lint-focused-tests.mjs:
- Match:
test.only,it.only, anddescribe.onlyfail. - Match:
test.skip,it.skip,describe.skip, and plaintest/it/describepass. - Match: focused identifiers inside comments or strings do not fail.
- Miss:
suite.only,fit, andfdescribepass under this rule. The CLI still flags those forms. - Miss: the rule runs only on test and spec file names. The CLI scans every parseable file.
Prefer a miss over a false positive. pnpm exec lint-drift catches gaps ESLint
misses.
Test hooks (vitest/no-hooks)
The shared config sets vitest/no-hooks to error with beforeAll and
afterAll allowed. That bans beforeEach and afterEach in test and spec
files.
Tradeoff versus scripts/lint-before-each-after-each.mjs:
- Match:
beforeEachandafterEachfail in test files. - Match:
beforeAllandafterAllpass. - Miss: the rule runs only on test and spec file names. The CLI scans every parseable file.
Prefer a miss over a false positive. pnpm exec lint-drift catches gaps ESLint
misses.
Nested describe (vitest/max-nested-describe)
The shared config sets vitest/max-nested-describe to error with max: 1.
That allows one top-level describe and one nested describe per test file.
Tradeoff versus scripts/lint-nested-describe.mjs:
- Match: a third nested
describefails. - Miss: the rule runs only on test and spec file names. The CLI scans every parseable file.
Prefer a miss over a false positive. pnpm exec lint-drift catches gaps ESLint
misses.
React preset (eslint/react)
React apps can add a second flat config export:
@stryder-works/coding-standards/eslint/reactInstall eslint-plugin-react-hooks and eslint-plugin-jsx-a11y with this
package when you use the React preset.
Compose the recommended and React presets in the app eslint.config:
import codingStandards from "@stryder-works/coding-standards/eslint";
import codingStandardsReact from "@stryder-works/coding-standards/eslint/react";
export default [...codingStandards, ...codingStandardsReact];The React preset includes:
eslint-plugin-react-hooksflat recommendedeslint-plugin-jsx-a11yflat recommended- A
no-restricted-syntaxban on"use no memo"and"use no forget"directives in.jsxand.tsxfiles (React Compiler opt-outs) - Re-declares module-import
no-restricted-syntaxbans on.tsxand barrelindex.{ts,tsx}paths so those rules stay active after the React spread
Prefer a miss over a false positive for the compiler-opt-out ban: strings and
comments that mention those directives do not fail. The ban does not run on
plain .ts files.
[email protected] still declares an eslint peer through ^9
only. The plugin runs on ESLint 10. Until upstream widens that peer range, add
a package-manager override in the app repo when you use ESLint 10:
"pnpm": {
"peerDependencyRules": {
"allowedVersions": {
"eslint-plugin-jsx-a11y>eslint": "10"
}
}
}Module imports (barrels)
The shared config bans export * from in TypeScript files and bans re-export
barrels in index.{ts,tsx} files under common module roots (components,
features, lib, config, modules). See CODING_STANDARDS.md module-imports.
Tradeoff versus scripts/lint-own-repo-barrels.mjs:
- Match:
export * fromfails in any.ts,.tsx,.mts, or.ctsfile. - Match:
export { x } from './y'fails inindex.{ts,tsx}under the default module-root globs. - Match: a facade
index.tsthat exports local symbols or functions passes. - Miss:
export { x } fromoutsideindex.{ts,tsx}passes under this rule. The CLI still flags own-repo package-name and import-map re-exports in any file. - Miss:
index.{ts,tsx}outside the default module-root globs passes. Override the globs when your app uses different roots.
Prefer a miss over a false positive. pnpm exec lint-drift catches gaps ESLint
misses.
Override barrel index globs by importing the modules factory:
import codingStandards from "@stryder-works/coding-standards/eslint";
import { createModulesConfig } from "@stryder-works/coding-standards/eslint/modules";
export default [
...codingStandards,
...createModulesConfig({
barrelIndexFiles: [
"**/widgets/**/index.{ts,tsx}",
"**/components/**/index.{ts,tsx}",
],
}),
];The factory export is also available without the full recommended spread:
@stryder-works/coding-standards/eslint/modulesBoundaries (eslint-plugin-boundaries)
eslint-plugin-boundaries is an optional app peer. This package does not ship
a default element graph or boundary rules.
Each app owns its elements map, dependency policies, and boundaries/* rules
in the app eslint.config. One app may use feature and module elements. Another
app may use membership and Square module elements.
Do not add shared boundary rules here that assume a folder layout.
Pre-commit hook
In the app repo's .husky/pre-commit (edit the app repo yourself — do not
open PRs from this repo), run ESLint on staged files via lint-staged. Compose
the shared config from this package in the app's lint-staged config (or
eslint.config).
pnpm exec lint-stagedAdd the app repo's own typecheck and test after that line.
Drift audit
Run the full CLI proof suite on a schedule (see Enforcement model above):
pnpm exec lint-driftOr invoke individual bins in --tree mode when debugging one rule:
pnpm exec lint-comments --tree
pnpm exec lint-empty-catch --treeStaged-mode CLI bins (default, no --tree) remain available for this repo's own
hook and for custom workflows.
Point the app repo's AGENTS.md at the installed docs (do not copy them):
node_modules/@stryder-works/coding-standards/CODING_STANDARDS.mdnode_modules/@stryder-works/coding-standards/docs/agents/testing-principles.md
Or use the GitHub URLs:
- https://github.com/Stryder-Works/coding-standards/blob/main/CODING_STANDARDS.md
- https://github.com/Stryder-Works/coding-standards/blob/main/docs/agents/testing-principles.md
Project-specific standards (domain context, trade dress, forge workflow) stay in each app repo.
