| name | asset_pipeline--lint-conventions |
| description | Run the asset_pipeline convention linter (`scripts/lint_conventions.cr`) — Crystal-side runner that enforces Phase 10 Family 1 naming rules across src/, samples/, and spec/. Use before commit, in CI, or when reviewing a PR. |
asset_pipeline — Convention Linter
Convention enforcement for asset_pipeline ships as a Crystal-side
runner, not as an AmberLSP extension. The runner walks the repo,
applies every registered rule, and exits non-zero on violations so
CI and pre-commit hooks can gate.
When to use
- Before committing on a feature branch.
- In CI as a fast pre-build gate.
- During PR review when you suspect naming drift.
- When you add a new screen, controller, or view subclass.
Invocation
crystal run scripts/lint_conventions.cr
Exit codes:
0 — no diagnostics.
1 — one or more diagnostics emitted (printed to stdout).
2 — bad CLI arguments.
Output line format:
<file>:<line>: [<rule_name>] <message> (suggested: <fix>)
Useful options
crystal run scripts/lint_conventions.cr -- --rules=family_1
crystal run scripts/lint_conventions.cr -- --no-default-paths --paths=src/ui/views
crystal run scripts/lint_conventions.cr -- --format=machine
Default scan roots: src/, samples/, spec/. Always-excluded:
lib/, .crystal-cache/, and spec/fixtures/ (test fixtures
intentionally exercise malformed shapes).
Suppression
Add # lint:disable=<rule_name> on its own line near the top of a
file to suppress that rule across the whole file. Multiple rules can
be listed comma-separated. # lint:disable=all disables every rule
for the file. Use sparingly; prefer fixing the violation.
Rule families
Family 1 — naming (this sub-phase)
Five regex-based rules covering screen/controller/view naming:
| Rule name | What it checks |
|---|
family_1/screen_class_naming | Every < UI::Screen subclass name ends in Screen. |
family_1/screen_file_suffix | Files declaring a < UI::Screen subclass use the matching foo_screen.cr basename. |
family_1/controller_class_naming | Every < UI::Controller subclass name ends in Controller. |
family_1/controller_file_suffix | Files declaring a < UI::Controller subclass use the matching foo_controller.cr basename. |
family_1/view_subclass_under_views_dir | UI::View subclasses live under src/ui/views/ or samples/. |
Family 2 — view-spec pair (deferred to 10A.0b)
Family 3 — architectural (deferred to 10A.0c)
Family 4 — test_id hygiene (deferred to 10A.final)
Family 5 — directory + multi-target (10C.0 + 10A.final)
Adding a new rule
Rules are auto-discovered. Drop a file and ship — no runner edit
required.
- Create the rule file under
src/lsp_rules/family_<N>_<topic>/<name>_rule.cr.
- Subclass
ConventionRule and implement rule_name : String +
check(file_path, content) : Array(Diagnostic). (Optional:
override configure(config : ConventionConfig) to receive runtime
config.)
- The class auto-registers via
ConventionRule.inherited, and the
runner auto-requires every *_rule.cr under src/lsp_rules/ via a
compile-time find macro expansion.
- Run
crystal run scripts/lint_conventions.cr and confirm the rule
count went up by one and the run is green on src/ + samples/.
- Manually break a file; confirm red on injection.
- Add a SKILL.md row in the appropriate Family table.
To verify auto-discovery, drop a stub rule file in any
src/lsp_rules/family_N_*/ directory, re-run the linter, observe the
rule count increment, then delete the file.
Configurable allowlists
Some rules read a config file at the repo root:
.lint_conventions.yml — flat YAML with sectioned keys.
Currently supported keys:
view_subclass:
approved_roots:
- "src/ui/views/"
- "samples/"
If the config file is missing, defaults apply. Pass --config=<path>
to override the location.
Regression fixtures
Known false-positive cases are pinned as fixtures under
spec/lint_conventions/fixtures/. A fixture file is a single .cr
file with a comment header naming the expected outcome (pass or fail
with which rule). The optional spec at
spec/lint_conventions/family_1_naming_spec.cr exercises each rule
against the fixtures to lock in current behavior.
CI integration
- name: Convention lint
run: crystal run scripts/lint_conventions.cr
Pre-commit hook
#!/usr/bin/env bash
set -e
crystal run scripts/lint_conventions.cr --quiet
Distribution
The skill ships in asset_pipeline's .claude/skills/. Per the
shards AI-docs auto-detection rule, consumers receive the skill
installed under their .claude/skills/asset_pipeline--<skill_name>/
namespace. Because this skill's source dir is already prefixed with
asset_pipeline-- (matching the brief), shards installs it under a
doubly-prefixed path. See the phase-10-a-0a-close.md handoff.
Why a Crystal runner instead of AmberLSP?
AmberLSP's CustomRule is YAML-regex-only and has no Crystal
compiled rule loader. Per the Phase 10 architecture decision
(2026-05-25), convention rules ship as a Crystal program. Rule
classes live under src/lsp_rules/ so they port directly if AmberLSP
later grows the loader.