Skip to content

Latest commit

 

History

History
238 lines (171 loc) · 21.8 KB

File metadata and controls

238 lines (171 loc) · 21.8 KB

cosmostrix Project Rules

Source file size

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/.

Module organization

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.

Validation

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-all

License headers

All core, config, and script files must carry an SPDX license identifier. See scripts/check-headers.sh for the enforced format.

Code quality

  • Clippy must pass with -D warnings (warnings are errors).
  • cargo fmt must report no differences.
  • All tests must pass on every commit.
  • MSRV: Rust 1.98.0 (pinned in rust-toolchain.toml).

Test discipline

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.

Cosmic Dragon Architecture

Atmosphere Engine (REMOVED 2026-08-05)

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).

Live Config Reload + Config Validation

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).

CLI Flag Policy

  • 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 ... exists format; the legacy Did you mean format was fully removed):

    • Long-flag typos (--scne): clap's built-in suggestions feature (jaro similarity) + extract_clap_suggestion() in src/cli/suggestion.rs, which reads clap's OWN tip: line and appends tip: 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-effects rename drift bug).
    • Enum-value typos on clap value_enum flags (--intro, --msg-fill-style, --bench-scene, ...): clap's built-in tip: a similar value exists.
    • Enum-value typos on prevalidator-intercepted flags (--glitch-level, --monolith-size, --color-bg): validate_enum_value() in src/validation/mod.rs appends tip: a similar value exists: '<value>' via the shared format_value_suggestion + closest_value_match (edit distance ≤ 2).
    • Color values (-c/--color): closest_color_name() in src/cli/mod.rs (edit distance ≤ 2 over builtin names + aliases).
    • Scene values (--scene): scene_suggestion_tip() in src/config/config_apply.rs (builtin scenes + [scene-custom.<name>] blocks).
    • Charset values (-C/--charset/--charset-custom): charset_from_str() suggests from CHARSET_PRESET_NAMES (custom [charset-custom.<name>] names are listed by --list-charsets but 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() in src/scene_custom/mod.rs.
    • Short-flag shorthand typos (-mfss): cli/argv_expand.rs exits with Did you mean --msg-fill-style?.
    • Removed flags (--preset, --low-power, ...): migration-hint table in src/validation/mod.rs (REMOVED_FLAGS — a different concern: renamed/removed flags get migration guidance, not typo suggestions).
    • Unknown config.toml keys: config_hints suggests known-key patterns.
    • Shared engine: edit_distance + closest_value_match live in src/cli/suggestion.rs (edit distance ≤ 2, case-insensitive, deterministic first-best tie-break) — the same policy everywhere.
  • Quit: only q exits. 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 _ => {} in handle_keybinding).

  • Active runtime keybinds (complete set, see --help RUNTIME CONTROLS):

    • q Quit · r Reset animation + restart message typewriter · c/C cycle color scheme fwd/back · s/S cycle charset preset fwd/back · p pause/resume · x/X cycle scene fwd/back · Up/Down speed up/down · [/] density down/up · i toggle live HUD
    • Pause isolation: while paused or decelerating toward pause (cloud.is_paused_or_decelerating()), ONLY p (resume) and q (quit) respond — every other key is silently ignored, i included (i is dispatched in the event loop BEFORE handle_keybinding, so it is gated by input::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/x accept 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 the is_unmodified_or_shift allowlist.
    • Kitty-protocol terminals (kitty, Alacritty, WezTerm, ghostty, foot, konsole) report Shift+letter as the BASE lowercase codepoint + SHIFT (CSI 120;2u for Shift+X); legacy terminals report the shifted uppercase char. normalize_shifted_char() in src/interactive/input.rs maps 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 q exits.

  • Exhaustive no-op lock (v80.0.0-beta.1 audit): src/interactive/tests_v51_shortkey_noop.rs verifies 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 (only p returns true from handle_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 with i, not h); Tab/BackTab explicit no-op arm (now catch-all; historical shading-mode toggle that caused phosphor ghost flood — see tests.rs::tab_*). Stale doc references to a, m, g, b/B as "interactive" keys were purged — these were never active.

  • Removed flags (each has a migration error in src/validation/mod.rs REMOVED_FLAGS table): 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).

Config Path Whitelist (Security)

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 Output + Install Script

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.

Naming Collision Policy (v50.0.0-beta.6 Option D)

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.

Custom Block LTS Bounds (v50.0.0-beta.6)

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.

Config value quoting invariant (bug #19, v80.0.0-beta.1)

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 quoted set value 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).

Killer-feature warning routing (v80.0.0-beta.1 hardening)

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.

Dynamic dsty: Metric (v50.0.0-beta.6 Option D + v80.0.0-beta.1 banded masterclass)

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 space
  • compute_spawn_scale() is a shared function (central_control_rains/density_throttle.rs) — the same function used by rain_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; raw power_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.