All Rust source files under src/ must stay under 800 gross lines (hard limit, enforced by scripts/check-rs-loc.sh). The soft target for new files is 500 lines. See src/RULES_LOC.md for the full policy (when to split, when NOT to split, generated-code exemption, migration path from the previous 1500-line cap).
Scope: src/**/*.rs, build.rs, *.toml, .cargo/config.toml, rust-toolchain.toml, *.sh, scripts/*.sh, benchmark/*.sh, .github/workflows/*.yml, .github/FUNDING.yml. Excluded: *.md, docs/**/*.md, *.txt, assets, images, videos, Cargo.lock, target/, .git/.
Prefer splitting modules by responsibility over allowing large files. main.rs should remain bootstrap and wiring only (target 100–300 LOC long-term). cli.rs may be larger if it contains mostly Clap command definitions, but must stay under 800 LOC (hard limit). Module directories (e.g. src/engine/cosmic_dragon_engine/cloud/, src/interactive/) use mod.rs as the public entry point and split implementation into focused submodules (soft target 500 LOC per file). Tests are colocated with their module in dedicated tests/ subdirectories.
Behavior-preserving refactors must pass the full validation suite:
scripts/check-rs-loc.sh
scripts/check-headers.sh
cargo fmt --all
cargo test --all --locked
cargo clippy --locked --all-targets --all-features -- -D warnings
./scripts/build.sh check-allAll core, config, and script files must carry an SPDX license identifier. See scripts/check-headers.sh for the enforced format.
- Clippy must pass with
-D warnings(warnings are errors). cargo fmtmust report no differences.- All tests must pass on every commit.
- MSRV: Rust 1.98.0 (pinned in
rust-toolchain.toml).
Tests must verify behavior, never identity. A tautological assertion (a constant matching itself) provides zero information and breaks the suite on every unrelated change.
Forbidden (tautological version assertions — Cargo.toml/PKGBUILD/README always contain their own version):
assert!(include_str!("../Cargo.toml").contains("version = \"5.0.1\""));
assert!(include_str!("../aur/cosmostrix-bin/PKGBUILD").contains("pkgver=5.0.1"));
assert!(include_str!("../README.md").contains(r#"TAG="v5.0.1""#));Allowed (dynamic, via env!() — single source of truth):
const CURRENT_VERSION: &str = env!("CARGO_PKG_VERSION");
assert!(include_str!("../Cargo.toml").contains(&format!("version = \"{}\"", CURRENT_VERSION)));
assert!(include_str!("../aur/cosmostrix-bin/PKGBUILD").contains(&format!("pkgver={}", CURRENT_VERSION)));Forbidden: test-on-test meta-pattern — tests must not assert that other test files contain a particular literal string (e.g. assert!(p14.contains("3.1.0"))). Every version bump would force manual edits across multiple test files just to satisfy one meta-test.
Allowed: historical CHANGELOG assertions (e.g. assert!(changelog.contains("## v13.0.0")), assert!(changelog.contains("## v50.0.0-alpha.5"))) — those entries are immutable historical record and remain valid forever.
Enforcement: scripts/check-version-anti-patterns.sh (run by build.sh check-all) scans src/**/*.rs for forbidden patterns and fails the build if detected: contains("version = \"X.Y.Z\""), contains("pkgver=X.Y.Z"), contains(r#"TAG="vX.Y.Z""#). If a future test genuinely needs the current package version, use env!("CARGO_PKG_VERSION") — never hardcode the literal string.
Fully eliminated at commit 07b44b5 (Dragon Hunt v2 Phase 6 Tier E item 31). All src/atmosphere_*.rs source files, --atmosphere-mode / --atmosphere-regime CLI flags, atmosphere-mode / atmosphere-regime / adaptive-custom.* config keys, and atmosphere-* scene-custom presets have been removed. Historical reference: docs/archive/specs/ATMOSPHERE_ENGINE.md (design spec), docs/archive/specs/CINEMATIC_BREATHING.md (vocabulary spec), docs/archive/audits/ATMOSPHERE_SUBSYSTEM_ARCHIVAL.md (full elimination record). Subsystems still sharing the "atmosphere" name but NOT deleted (separate subsystems): src/engine/chroma_dragon_engine/post/climate/mod.rs (Chroma Dragon post-FX shader), AtmosphericEvolution struct in src/engine/cosmic_dragon_engine/cloud/ecosystem.rs (cloud drift/gust events).
Watches config.toml via notify crate (background thread). Full Cloud rebuild on change (not delta apply). --testconf validates all keys + values strictly. Startup rejects invalid config (exit 2). Live reload rejects invalid config (exit 2, error printed to stderr AFTER terminal restore). Malformed lines (no = or empty key/value) -> error. Unknown keys -> error. Invalid values -> error. No silent fallback. No warnings. Errors only. Modules: live_config.rs, testconf.rs (shared validation).
-
Suggestion coverage (v80.0.0-beta.1 audit + Z-master-1X consistency audit — every CLI/value surface suggests on typos, unified to the
tip: a similar ... existsformat; the legacyDid you meanformat was fully removed):- Long-flag typos (
--scne): clap's built-insuggestionsfeature (jaro similarity) +extract_clap_suggestion()insrc/cli/suggestion.rs, which reads clap's OWNtip:line and appendstip: a similar argument exists: '--<flag>'(the two lines can never disagree — the custom Levenshtein engine + hand-maintained KNOWN_LONG_FLAGS list were removed in v50.0.0-beta.7 after the--disable-effectsrename drift bug). - Enum-value typos on clap
value_enumflags (--intro,--msg-fill-style,--bench-scene, ...): clap's built-intip: a similar value exists. - Enum-value typos on prevalidator-intercepted flags (
--glitch-level,--monolith-size,--color-bg):validate_enum_value()insrc/validation/mod.rsappendstip: a similar value exists: '<value>'via the sharedformat_value_suggestion+closest_value_match(edit distance ≤ 2). - Color values (
-c/--color):closest_color_name()insrc/cli/mod.rs(edit distance ≤ 2 over builtin names + aliases). - Scene values (
--scene):scene_suggestion_tip()insrc/config/config_apply.rs(builtin scenes +[scene-custom.<name>]blocks). - Charset values (
-C/--charset/--charset-custom):charset_from_str()suggests fromCHARSET_PRESET_NAMES(custom[charset-custom.<name>]names are listed by--list-charsetsbut not suggested — the parser has no config access). - Custom palette names (
--colors-custom,color =):load_custom_palette()suggests from the defined[colors-custom.<name>]blocks. - Custom scene names (
--scene-custom):unknown_custom_scene_error()insrc/scene_custom/mod.rs. - Short-flag shorthand typos (
-mfss):cli/argv_expand.rsexits withDid you mean --msg-fill-style?. - Removed flags (
--preset,--low-power, ...): migration-hint table insrc/validation/mod.rs(REMOVED_FLAGS — a different concern: renamed/removed flags get migration guidance, not typo suggestions). - Unknown config.toml keys:
config_hintssuggests known-key patterns. - Shared engine:
edit_distance+closest_value_matchlive insrc/cli/suggestion.rs(edit distance ≤ 2, case-insensitive, deterministic first-best tie-break) — the same policy everywhere.
- Long-flag typos (
-
Quit: only
qexits. Esc, Ctrl+C (SIGINT deprecated — only SIGTERM/SIGHUP/SIGQUIT trigger graceful shutdown), Ctrl+Z (in-app suspend removed; OS SIGTSTP still works), Tab/BackTab, and all other unrecognized keys are silently ignored (catch-all_ => {}inhandle_keybinding). -
Active runtime keybinds (complete set, see
--helpRUNTIME CONTROLS):qQuit ·rReset animation + restart message typewriter ·c/Ccycle color scheme fwd/back ·s/Scycle charset preset fwd/back ·ppause/resume ·x/Xcycle scene fwd/back ·Up/Downspeed up/down ·[/]density down/up ·itoggle live HUD- Pause isolation: while paused or decelerating toward pause (
cloud.is_paused_or_decelerating()), ONLYp(resume) andq(quit) respond — every other key is silently ignored,iincluded (iis dispatched in the event loop BEFOREhandle_keybinding, so it is gated byinput::hud_toggle_accepted()which applies the same predicate). While paused, HUD running metrics freeze (see docs/HUD.md) and mouse click waves are suppressed. - Shift is the ONLY accepted modifier (owner policy). Cycle keys
c/s/xaccept their uppercase form (C/S/X= reverse cycle, produced by Shift or CapsLock); every other key responds only to the bare lowercase press. Ctrl/Alt/Super/Hyper/Meta/Fn combinations are unconditionally rejected by theis_unmodified_or_shiftallowlist. - Kitty-protocol terminals (kitty, Alacritty, WezTerm, ghostty, foot, konsole) report Shift+letter as the BASE lowercase codepoint + SHIFT (
CSI 120;2ufor Shift+X); legacy terminals report the shifted uppercase char.normalize_shifted_char()insrc/interactive/input.rsmaps both shapes to the same match arms, so Shift+X/C/S reverse-cycle identically on both terminal families.
-
Screensaver mode: all the above keys work normally. Only
qexits. -
Exhaustive no-op lock (v80.0.0-beta.1 audit):
src/interactive/tests_v51_shortkey_noop.rsverifies that every key OUTSIDE the active set is a complete no-op — no redraw, no state change (color scheme, charset, scene, density, speed, pause, raining, async_mode all pinned). Covers the owner's exact scenario (a— old-version async-toggle muscle memory), every non-active letter a–z/A–Z, digits, punctuation, the removed density aliases (-/_/+/=), and non-active special keys (Tab/BackTab/Enter/Backspace/Delete/Insert/Home/End/PageUp/PageDown/Left/Right/Esc/F-keys). Positive controls assert every ACTIVE key still has its documented effect (onlypreturns true fromhandle_keybinding; the other active keys signal redraw via internal force-draw flags). -
Removed legacy keybinds (silently ignored via catch-all, were never in
--help):-_+=(density aliases for[/]);Ctrl+Z(in-app suspend);h(HUD position toggle — completely removed, no binding exists, silently ignored; HUD always renders flush-left at column 0 per v50.0.0-beta.6; HUD visibility is toggled withi, noth);Tab/BackTabexplicit no-op arm (now catch-all; historical shading-mode toggle that caused phosphor ghost flood — seetests.rs::tab_*). Stale doc references toa,m,g,b/Bas "interactive" keys were purged — these were never active. -
Removed flags (each has a migration error in
src/validation/mod.rsREMOVED_FLAGStable): v14.0.0 (--preset,--profile,--low-power,--list-presets,--list-profiles,--show-preset,--dump-profile,--list-colors-detail,--defaults,--tune-visual); v15.0.0 (--completions <shell>); v17.0.0 (--mouse,--info/-i,--async/-a,--brightness/--saturation,--glitchpct/--shortpct/--rippct/--maxdpc); v25.0.0 (--charset-file <path>); v25.0.0-alpha.3 (--fullwidth). -
Android/Termux: accept Press + Repeat key events (skip Release).
Removed feature (v80.0.0-beta.2): the density-map per-column spawn-weight config field was retired — a burden function that was rare to use and costly to maintain (CSV parser, Box::leak dedup cache, entry cap, testconf validation, generator script, doc surface). Monolith spawn distribution is uniform now; the value-noise density in living_rain.rs is untouched (different subsystem). Configs still carrying scene-custom.<name>.density-map or a top-level density-map get a targeted removal hint from config_hints. scripts/gen-density-presets.py was deleted.
Config path whitelist (enforced by safepath.rs): Linux ~/.config/cosmostrix/, /etc/cosmostrix/; macOS ~/.config/cosmostrix/, ~/Library/Application Support/cosmostrix/, /etc/cosmostrix/; Windows %APPDATA%\cosmostrix\, %ProgramData%\cosmostrix\. Rejected: current directory, /tmp/, ~/.local/, /usr/, all others.
Verbose: startup dumps full config to stderr (no borders, purple brand color). Runtime: changes tracked silently (no eprintln during rain — causes flicker). After exit: final runtime state section always prints (v50.0.0-beta.6) — first line is exit_time: <YYYY-MM-DD HH:MM:SSZ> (UTC, ISO 8601) and duration: <Xm Ys> showing the total process lifetime. UTC chosen for LTS stability (no DST transitions, no tzdata drift). Changed live-reload fields follow (only if any value changed during the session). Format: [verbose] field: value (was old_value). The section closes with the ambient diagnostics summary.
Install: ./scripts/install auto-detects CPU — AVX-512 -> pro-linux-v4, AVX2 -> pro-linux-v3, baseline -> release. --system flag: install to /usr/bin. Default: ~/.local/bin.
When a custom config block ([charset-custom.<name>], [colors-custom.<name>], [scene-custom.<name>]) has the same name as a builtin preset/scene/theme, custom always wins. A collision warning is emitted to stderr at startup so the user knows the builtin is being shadowed:
Warning: warning: custom charset 'zen' overrides builtin — custom wins (Option D policy)
builtin: builtin preset (see --list-charsets)
custom: 1 char(s) from [charset-custom.zen]
To use the builtin, rename the custom block in config.toml.
This policy is consistent across all 3 systems (charset, colors, scene). Previously they had inconsistent behavior: charset was custom-wins, colors was builtin-wins, scene was builtin-wins. Now all three are custom-wins with visible warning. The user can always use the explicit flags (--colors-custom, --scene-custom, --charset-custom) for unambiguous intent.
All 3 custom config systems use the same bounds for consistency. Max 100 blocks per category (generous — built-in themes are ~44, built-in scenes ~10, built-in charsets ~25). Max 64 char names (built-in names are ≤16 chars; longer = likely typo).
Unified bounds table:
| System | Max blocks | Max name len | Max content | Rationale |
|---|---|---|---|---|
| colors-custom | 100 (COLORS_CUSTOM_MAX_BLOCKS) |
64 (COLORS_CUSTOM_MAX_NAME_LEN) |
64 rain stops (COLORS_CUSTOM_MAX_RAIN_STOPS) |
OKLab engine only needs 2-16 stops; 100 blocks far exceeds realistic use |
| charset-custom | 100 (CHARSET_CUSTOM_MAX_BLOCKS) |
64 (CHARSET_CUSTOM_MAX_NAME_LEN) |
256 chars (CHARSET_CUSTOM_MAX_LEN) |
Bounded glyph pool; prevents 10K-char paste bloat |
| scene-custom | 100 (SCENE_CUSTOM_MAX_BLOCKS) |
64 (SCENE_CUSTOM_MAX_NAME_LEN) |
— | v80.0.0-beta.2 removed the density-map field (and its 1024-entry cap) entirely; no content cap remains for scene blocks |
When a cap is hit, behavior depends on the cap type:
- Content cap (rain stops, charset chars): emits a runtime warning via
push_runtime_warning(drained after Terminal::drop so it doesn't leak into the rain matrix). Example:colors-custom: rain stops capped at 64 (extra stops ignored). - Block cap (total blocks per category): silently skipped (no warning — the user would have to define 100+ blocks to hit this, which is almost certainly a script-generated config, not a human typo).
- Name length cap: silently skipped (no warning — almost certainly a typo, warning would be noise).
All 3 systems are now aligned: same max blocks (100), same max name len (64), same skip semantics. This makes the LTS contract predictable across colors, charset, and scene custom blocks.
A double-quoted value is NEVER an array. parse_config_text snapshots
raw_is_quoted (value starts AND ends with ") BEFORE quote-stripping,
and both array branches (the bug #7 unquoted-# rejection and the v25
multi-line array consumer) are gated on !raw_is_quoted. Reason: a
charset like set = "[" (single-bracket glyph) quote-strips to a bare
[, which the array consumer used to mistake for an unterminated array —
rejecting the whole line as "array never closed" or silently absorbing
following lines. Owner-found 2026-08-30 while previewing single-glyph
charset candidates.
Consequences, locked by src/config/configfile_tests/bug19.rs and the
charset-custom validation tests:
- Any single-width glyph — including
[,],#,=— is a legal quotedsetvalue and is stored verbatim. - Unquoted
[values remain array openers (bug #7 semantics unchanged). - No escape sequences exist: a lone
"glyph is not expressible (the quote is the string delimiter); mid-pool quotes are kept. - Duplicate
[section]headers are last-wins in the forgiving parser (same key overwrite, no error).
Warnings that can fire on BOTH sides of the interactive session boundary (charset
wide-char skip notes, custom-vs-builtin name collisions, scene-custom invalid
field notes) route through output::warn_runtime_or_now: direct stderr before
the rain session starts, buffered push_runtime_warning while the alternate
screen is active (AB-10 — a stderr line must never leak into the rain matrix).
The session gate is INTERACTIVE_SESSION_ACTIVE, set once at the top of
run_interactive. The runtime warning log also dedups identical messages —
several killer-feature notes re-fire per scene change / config save, and the
post-exit summary must stay readable.
The dsty: HUD metric (row 12) is dynamic when power-dragon is ON — it reflects the effective density after the v80.0.0-beta.1 banded throttle. When power-dragon is OFF, dsty: is static (shows the user's configured density — v80.0.0-beta.1 also gates the pressure FEED itself to 0.0, so the render path matches the display).
How it works:
dsty:=user_density * compute_spawn_scale(pressure, aggressive, user_density)— the target lands on the banded curve in ABSOLUTE density spacecompute_spawn_scale()is a shared function (central_control_rains/density_throttle.rs) — the same function used byrain_at()in the render path. No formula drift.pressure= the APPLIED pressure (v80.0.0-beta.1: gated to 0.0 when power-dragon is off; rawpower_manager.effective_pressure()otherwise)aggressive=cloud.aggressive_throttle(set by self-healer on sustained high CPU; released when power-dragon turns off)- v80.0.0-beta.1 bands: dead zone p <= 0.05, low 0.84-0.70, medium 0.70-0.50, high (rare) 0.50-0.10; aggressive reads the pressure +0.20 deeper (same band edges)
Behavior table:
| State | dsty: shows |
Example |
|---|---|---|
| power-dragon OFF | user density (static, feed gated) | dsty: 0.75 |
| power-dragon ON, pressure in dead zone (p <= 0.05) | user density (full) | dsty: 0.72 |
| power-dragon ON, 50% pressure | medium band | dsty: 0.57 (0.72-ceiling, band target 0.5667) |
| power-dragon ON, 60% pressure | medium-band floor | dsty: 0.50 (monolith 0.85's owner-observed regime) |
| power-dragon ON, 100% pressure | high-band floor | dsty: 0.10 |
| power-dragon ON + aggressive | reads one band deeper | dsty: 0.40 (p=0.5 reads 0.7) |
CLI --density 1.0 + max pressure |
CLI is the ceiling, throttle reduces | dsty: 0.10 |
| cheap scene (density 0.30), medium pressure | untouched (below band edges) | dsty: 0.30 |
CLI wins: the user's configured density (CLI -d > config density > scene builtin) is the ceiling — the throttle only ever reduces below it, never above it. Cheap scenes (density below the band edges) self-harmonize: they stay untouched until the deep bands cross them.
Custom blocks have a strict field allowlist — unknown fields are rejected as errors, NOT auto-promoted to root scope. This prevents silent side-effects like color = green inside [charset-custom.quantum] changing the global color scheme.
Allowed fields per block type:
| Block | Allowed fields | Source |
|---|---|---|
[colors-custom.<name>] |
bg, rain, stops (deprecated alias) |
is_valid_colors_custom_field() |
[charset-custom.<name>] |
set only |
is_valid_charset_custom_field() |
[scene-custom.<name>] |
base-scene, color, charset, bold, colors-custom, charset-custom, shading-mode, glitch-level, fps, speed, density, async-mode |
SCENE_CUSTOM_FIELDS |
Any other field inside these blocks surfaces as an unknown_key -> --testconf reports the error, live-reload rejects the config. The auto-promote path (which previously moved top-level keys like color/intro/speed from inside a custom block to root scope) is disabled when current_section starts with charset-custom., colors-custom., or scene-custom..
Auto-promote still works for non-custom sections (e.g. [color.tune] — a top-level key accidentally nested under it still promotes to root). Only custom blocks are strict.