under200
A discipline skill that adapts NASA's Power of Ten rules (Gerard J. Holzmann, JPL) into general-purpose software engineering practice. The central, non-negotiable rule is the 200-line file ceiling. Every other rule exists to make that ceiling achievable without smuggling complexity elsewhere.
Why 200 lines
A reviewer should be able to open a file, scroll once, and have read the whole thing. Files longer than that force the reader into the jump-around-and-hold-state-in-your-head mode that hides bugs, defeats code review, and resists onboarding. 200 lines is roughly two editor screens at a comfortable font size — enough room for a real unit of code, short enough that nothing is hiding.
This is a ceiling, not a target. Most files should be much smaller. If a file is at 195 lines, it is already overdue for a split.
The rules
Apply these in priority order. The earlier rules win when they conflict with later ones.
1. Files stay under 200 lines (hard ceiling)
- Count all lines including imports, blank lines, and comments. The reviewer has to scroll past them too.
- Generated files, lockfiles, fixtures, and migrations are exempt — they are not read top-to-bottom.
- When a file crosses 200 lines, split it before adding the feature that pushed it over, not after. Splitting under deadline pressure produces worse seams than splitting deliberately.
- Prefer splits along meaning (one concept per file), not along line count (don't just cut at line 200).
2. Functions stay under ~60 lines
One printed page. If a function does not fit on a screen, the reader cannot hold its control flow in working memory. Extract helpers — but extract along meaningful boundaries, not arbitrarily.
3. Control flow stays simple
- No
goto. No setjmp/longjmp analogues (uncontrolled exception-as-control-flow).
- Recursion only when bounded by a known finite input (parsing a fixed-depth AST is fine; recursing over user input is not).
- Every loop must have a provable upper bound on iterations.
while (true) is acceptable only with a clear exit invariant documented in code.
4. Validate at boundaries, trust internals
- Check the return value of every fallible call. If a return value is intentionally ignored, make that explicit (
_ = … in Go, void cast in C, # noqa with rationale in Python).
- Validate inputs at system boundaries (HTTP handlers, CLI entry points, deserialization). Do not re-validate inside internal helpers — that is defensive bloat and inflates line counts.
5. Smallest possible scope
- Declare variables at the narrowest scope where they are used.
- Prefer module-private over package-public; package-public over global.
- No mutable globals unless the runtime forces them (and then document why).
6. Limit metaprogramming and indirection
- Preprocessor macros (C/C++), decorators that rewrite ASTs, runtime monkey-patching, and deeply nested generics all hide control flow from the reader. Use them only when the alternative is materially worse.
- Function pointers, callbacks, and dynamic dispatch are fine — but each layer of indirection costs the reader something. Spend the budget where it pays off.
7. Compile/lint clean
- All warnings enabled. Treat warnings as errors in CI.
- Static analyzers and linters configured and passing. If a rule fires, fix the code; do not blanket-disable the rule.
When this skill triggers
Apply it whenever you are:
- Writing a new source file → start with the structure that will keep it under 200 lines.
- Editing an existing file → check the line count first. If it is over 150, plan the split before adding code.
- Reviewing code → flag any file over 200 lines and any function over 60.
- Setting up a new project, CI, or pre-commit config → configure the linter rules in the next section.
- Asked to "clean up" / "refactor" / "make this more readable" → the line ceiling is usually the fastest win.
Linter enforcement
If the project has a linter, configure it to enforce these rules. A rule that lives only in a skill rots; a rule that fails CI does not. When adding a new linter rule to an existing codebase that violates it, use the linter's per-file ignore mechanism for the existing offenders rather than relaxing the global threshold — that way new code is held to the standard and old code surfaces as known debt.
Concrete rule mappings (use these exact rules when the corresponding linter is present):
- ESLint / TypeScript:
max-lines: ["error", { max: 200, skipBlankLines: false, skipComments: false }], max-lines-per-function: ["error", 60], complexity: ["error", 10], max-depth: ["error", 4].
- Biome:
noExcessiveLinesPerFunction, noExcessiveCognitiveComplexity. File-length cap is not built in — add a pre-commit check.
- Ruff / Python: select
PLR0915 (too-many-statements), C901 (complexity), PLR0912 (too-many-branches). For file length use a pre-commit hook (check-added-large-files is for bytes; file-line cap needs a custom hook).
- Pylint:
max-module-lines=200, max-line-length=…, max-args, max-locals, max-branches.
- golangci-lint: enable
funlen (lines: 60), gocyclo (min-complexity: 10), lll. File length: enable filelength linter or add a CI check.
- Clippy / Rust:
clippy::too_many_lines (configure in clippy.toml with too-many-lines-threshold = 60), clippy::cognitive_complexity.
- RuboCop:
Metrics/ClassLength: Max: 200, Metrics/MethodLength: Max: 60, Metrics/CyclomaticComplexity, Metrics/AbcSize.
- SwiftLint:
file_length: 200, function_body_length: 60, cyclomatic_complexity.
- ktlint / detekt: detekt
LongMethod (threshold 60), LargeClass (threshold 200), ComplexMethod.
If the project has no linter, add a minimal pre-commit hook that fails when any tracked source file exceeds 200 lines. A POSIX one-liner is enough:
git diff --cached --name-only --diff-filter=ACM \
| grep -E '\.(ts|tsx|js|jsx|py|go|rs|rb|swift|kt|java|c|h|cpp)$' \
| xargs -I{} sh -c 'lines=$(wc -l < "{}"); [ "$lines" -le 200 ] || { echo "{}: $lines lines (>200)"; exit 1; }'
Wire it into Husky, pre-commit, lefthook, or whatever the project already uses. Do not invent a new hook framework.
How to apply during a task
- Before editing a file, run
wc -l <file>. If it is already ≥ 150 lines, plan how to split before adding code.
- After editing, re-check. If you crossed 200, split now — do not defer.
- When splitting, cut along the seam where coupling is weakest (one concept per file). If no such seam exists, the function/class is doing too much and needs decomposition first.
- If the user pushes back on a split ("just add it here, it's fine"), surface the cost ("this file will be 240 lines after the change, which makes review harder") and let them override. The rule is a strong default, not a religion — but the override should be conscious.
- When configuring a new project, add the linter rules from the table above in the same PR as the project scaffolding. Retrofitting limits later is painful.
What this skill is not
- Not a style guide (indentation, naming, import order). Use the project's existing formatter for that.
- Not a substitute for tests, types, or review.
- Not a license to split files arbitrarily — a 50-line file that is half of a coherent concept is worse than a 220-line file that is whole. Split meaning, not lines.
One-line summary
If a reviewer cannot read your file top-to-bottom in one scroll, the file is wrong — fix the file, and let the linter remember the rule so you don't have to.
1---2name: under2003description: Enforce NASA-style "Power of Ten" software engineering discipline on any code being written or modified, with a hard guideline that no source file exceeds 200 lines so a reviewer can read it top-to-bottom in one editor screen without jumping around. Use this skill whenever writing new source files, refactoring existing ones, reviewing code, splitting modules, or setting up a new project — even if the user does not explicitly say "under 200" or "NASA". If the user mentions file length, file size, "this file is too long", complexity, readability, code-review friendliness, or asks to set up linters/formatters/CI, apply this skill. Also apply proactively whenever any source file being edited is approaching or exceeds 200 lines.4---56# under20078A discipline skill that adapts NASA's *Power of Ten* rules (Gerard J. Holzmann, JPL) into general-purpose software engineering practice. The central, non-negotiable rule is the 200-line file ceiling. Every other rule exists to make that ceiling achievable without smuggling complexity elsewhere.910## Why 200 lines1112A reviewer should be able to open a file, scroll once, and have read the whole thing. Files longer than that force the reader into the *jump-around-and-hold-state-in-your-head* mode that hides bugs, defeats code review, and resists onboarding. 200 lines is roughly two editor screens at a comfortable font size — enough room for a real unit of code, short enough that nothing is hiding.1314This is a *ceiling*, not a target. Most files should be much smaller. If a file is at 195 lines, it is already overdue for a split.1516## The rules1718Apply these in priority order. The earlier rules win when they conflict with later ones.1920### 1. Files stay under 200 lines (hard ceiling)2122- Count all lines including imports, blank lines, and comments. The reviewer has to scroll past them too.23- Generated files, lockfiles, fixtures, and migrations are exempt — they are not read top-to-bottom.24- When a file crosses 200 lines, **split it before adding the feature that pushed it over**, not after. Splitting under deadline pressure produces worse seams than splitting deliberately.25- Prefer splits along *meaning* (one concept per file), not along *line count* (don't just cut at line 200).2627### 2. Functions stay under ~60 lines2829One printed page. If a function does not fit on a screen, the reader cannot hold its control flow in working memory. Extract helpers — but extract along meaningful boundaries, not arbitrarily.3031### 3. Control flow stays simple3233- No `goto`. No `setjmp`/`longjmp` analogues (uncontrolled exception-as-control-flow).34- Recursion only when bounded by a known finite input (parsing a fixed-depth AST is fine; recursing over user input is not).35- Every loop must have a provable upper bound on iterations. `while (true)` is acceptable only with a clear exit invariant documented in code.3637### 4. Validate at boundaries, trust internals3839- Check the return value of every fallible call. If a return value is intentionally ignored, make that explicit (`_ = …` in Go, `void` cast in C, `# noqa` with rationale in Python).40- Validate inputs at system boundaries (HTTP handlers, CLI entry points, deserialization). Do **not** re-validate inside internal helpers — that is defensive bloat and inflates line counts.4142### 5. Smallest possible scope4344- Declare variables at the narrowest scope where they are used.45- Prefer module-private over package-public; package-public over global.46- No mutable globals unless the runtime forces them (and then document why).4748### 6. Limit metaprogramming and indirection4950- Preprocessor macros (C/C++), decorators that rewrite ASTs, runtime monkey-patching, and deeply nested generics all hide control flow from the reader. Use them only when the alternative is materially worse.51- Function pointers, callbacks, and dynamic dispatch are fine — but each layer of indirection costs the reader something. Spend the budget where it pays off.5253### 7. Compile/lint clean5455- All warnings enabled. Treat warnings as errors in CI.56- Static analyzers and linters configured and passing. If a rule fires, fix the code; do not blanket-disable the rule.5758## When this skill triggers5960Apply it whenever you are:6162- Writing a new source file → start with the structure that will keep it under 200 lines.63- Editing an existing file → check the line count first. If it is over 150, plan the split before adding code.64- Reviewing code → flag any file over 200 lines and any function over 60.65- Setting up a new project, CI, or pre-commit config → configure the linter rules in the next section.66- Asked to "clean up" / "refactor" / "make this more readable" → the line ceiling is usually the fastest win.6768## Linter enforcement6970If the project has a linter, **configure it to enforce these rules**. A rule that lives only in a skill rots; a rule that fails CI does not. When adding a new linter rule to an existing codebase that violates it, use the linter's per-file ignore mechanism for the existing offenders rather than relaxing the global threshold — that way new code is held to the standard and old code surfaces as known debt.7172Concrete rule mappings (use these exact rules when the corresponding linter is present):7374- **ESLint / TypeScript**: `max-lines: ["error", { max: 200, skipBlankLines: false, skipComments: false }]`, `max-lines-per-function: ["error", 60]`, `complexity: ["error", 10]`, `max-depth: ["error", 4]`.75- **Biome**: `noExcessiveLinesPerFunction`, `noExcessiveCognitiveComplexity`. File-length cap is not built in — add a pre-commit check.76- **Ruff / Python**: select `PLR0915` (too-many-statements), `C901` (complexity), `PLR0912` (too-many-branches). For file length use a pre-commit hook (`check-added-large-files` is for bytes; file-line cap needs a custom hook).77- **Pylint**: `max-module-lines=200`, `max-line-length=…`, `max-args`, `max-locals`, `max-branches`.78- **golangci-lint**: enable `funlen` (lines: 60), `gocyclo` (min-complexity: 10), `lll`. File length: enable `filelength` linter or add a CI check.79- **Clippy / Rust**: `clippy::too_many_lines` (configure in `clippy.toml` with `too-many-lines-threshold = 60`), `clippy::cognitive_complexity`.80- **RuboCop**: `Metrics/ClassLength: Max: 200`, `Metrics/MethodLength: Max: 60`, `Metrics/CyclomaticComplexity`, `Metrics/AbcSize`.81- **SwiftLint**: `file_length: 200`, `function_body_length: 60`, `cyclomatic_complexity`.82- **ktlint / detekt**: detekt `LongMethod` (threshold 60), `LargeClass` (threshold 200), `ComplexMethod`.8384If the project has **no linter**, add a minimal pre-commit hook that fails when any tracked source file exceeds 200 lines. A POSIX one-liner is enough:8586```sh87git diff --cached --name-only --diff-filter=ACM \88 | grep -E '\.(ts|tsx|js|jsx|py|go|rs|rb|swift|kt|java|c|h|cpp)$' \89 | xargs -I{} sh -c 'lines=$(wc -l < "{}"); [ "$lines" -le 200 ] || { echo "{}: $lines lines (>200)"; exit 1; }'90```9192Wire it into Husky, `pre-commit`, lefthook, or whatever the project already uses. Do not invent a new hook framework.9394## How to apply during a task95961. **Before editing a file**, run `wc -l <file>`. If it is already ≥ 150 lines, plan how to split before adding code.972. **After editing**, re-check. If you crossed 200, split now — do not defer.983. **When splitting**, cut along the seam where coupling is weakest (one concept per file). If no such seam exists, the function/class is doing too much and needs decomposition first.994. **If the user pushes back** on a split ("just add it here, it's fine"), surface the cost ("this file will be 240 lines after the change, which makes review harder") and let them override. The rule is a strong default, not a religion — but the override should be conscious.1005. **When configuring a new project**, add the linter rules from the table above in the same PR as the project scaffolding. Retrofitting limits later is painful.101102## What this skill is *not*103104- Not a style guide (indentation, naming, import order). Use the project's existing formatter for that.105- Not a substitute for tests, types, or review.106- Not a license to split files arbitrarily — a 50-line file that is half of a coherent concept is worse than a 220-line file that is whole. Split *meaning*, not *lines*.107108## One-line summary109110If a reviewer cannot read your file top-to-bottom in one scroll, the file is wrong — fix the file, and let the linter remember the rule so you don't have to.