Skip to content

Design decisions

These are current, load-bearing choices from ryl's issues, pull requests, and repository history. Each Why records the published reason, or says "Not recorded" when no reason was found. Scope records declined requests, and the migration catalog records compatibility differences.

Precedent and trust

Decision Why Source
The YAML 1.2.2 grammar and reference parser decide parse disagreements. The reference event stream takes priority even when other parsers agree against it. The keep-scalar EOF case has the same reference value before and after formatting, while PyYAML and ruamel disagree; the spec's keep-empty production supports the reference reading. PyYAML and ruamel also accept YAML the spec rejects (yaml-test-suite 9C9N), so their agreement is no evidence of conformance. Spec and parser evidence, maintainer decision, 9C9N
Eemeli's yaml cross-checks the reference parser. PyYAML and ruamel are non-conformant and never outvote the reference, even 2–2. The recorded yaml-test-suite comparison found yaml closer to the reference on valid and invalid cases; the keep-scalar case demonstrates the split. Parser comparison, decision
Granit supplies ryl's parse events. Known divergences are recorded with examples in the migration catalog. A single parser stack avoids conflicting event streams; published divergences make cases where ryl differs from other parsers reviewable. Parser stack, divergence catalog
Configuration is explicit, local and fails loudly. Nothing runs from an unseen default, one nearest config governs each file, and anything ryl cannot apply is an error rather than a quiet warning. Opening one file should show exactly what runs, and a typo or unsupported key must not look effective. Explicit and local, unknown keys rejected
Ruff, rumdl, ty, and Biome are tool precedents. Check their installed binaries where possible; yamllint decides only where migration compatibility would break. ryl began from rumdl's example: a mostly single-developer Rust tool replacing Python Markdown linters showed the same could be done for YAML. rumdl itself draws on Astral's Rust tools (ruff, ty), so following their lead keeps ryl familiar to users of all three; yamllint remains the migration path, not the model. Contributor policy, formatter re-review

Configuration

Decision Why Source
Rules require explicit enablement. A missing config or one enabling no rules exits 2, so an unconfigured run cannot silently apply a preset or lint nothing. An unseen default preset would violate explicit rule selection; an empty selection would silently lint nothing. Decision
TOML is the native configuration model. Shared settings live at the top level, lint under [lint], and formatter targets under [format]. Migration warns and rewrites the former flat lint keys. Separate lint options from formatter targets while keeping common settings in one place; the shape matches Ruff's top-level, [lint] and [format] layout, so Ruff users meet no surprises. Epic, implementation
TOML configuration stays local. One selected config governs a file; TOML has no extends inheritance or implicit preset selection. One visible file should show every rule governing an input, without inheritance at a distance. Maintainer decision, Maintainer rationale
Legacy YAML configuration follows yamllint's surface. Ryl-only settings and rules belong in TOML; YAML rejects unknown rule names and TOML-only keys rather than silently ignoring them. YAML must remain a reliable yamllint migration surface; silently ignored keys make typos look effective. Decision, validation
Legacy YAML configuration is deprecated. Conversion remains available while warnings name a usable migration command; the removal release is not yet set. The TOML model can replace legacy execution only when existing YAML users have a working conversion path. Decision, warning
Deprecations need a usable target. A warning starts only after its replacement exists, and stale configuration must never silently stop applying. A warning before migration works would strand users, and silently dropping an old setting could change enforcement. Formatter epic
Shared indent-width and line-length are defaults. Formatter targets and lint defaults agree unless an explicit per-rule lint option overrides them; quote style remains formatter-owned. Shared defaults keep ordinary lint and format settings aligned while explicit lint tolerances remain possible. Decision, implementation
Discovery prefers the selected project config. Explicit CLI config wins first; project discovery precedes YAMLLINT_CONFIG_FILE, then native and yamllint-compatible user-global locations. A project config should govern its files before a compatibility environment variable or user-global setting. Discovery
Project discovery walks ancestors and .config/. A run from a subdirectory finds the governing project config, with native TOML candidates preferred in the documented order. Subdirectory runs must find the project config; .config/ supports an established repository layout. Ancestor fix, .config/ support
YAMLLINT_CONFIG_FILE accepts yamllint YAML only. Native TOML uses -c or project discovery, keeping the compatibility variable's meaning unambiguous. The yamllint-named variable should retain yamllint semantics, leaving native TOML to native discovery or -c. Decision
Ryl has a native user-global TOML path. The native path takes precedence over the yamllint-compatible user-global YAML file when no project config governs the input. Native users should not have to store ryl settings under a yamllint/ directory. Decision
Invalid configuration fails visibly. Unknown TOML keys or rule names are errors with exit 2, and the generated schema rejects them too. An ignored key can make an invalid configuration appear to work, so loader and schema should fail loudly. Rule validation, TOML validation
ALL and --enable are explicit shortcuts. They select all rules or a command-line set without an implicit default preset. Users need a concise explicit way to enable all rules or a CLI-selected set without restoring implicit defaults. Decision
Per-file and per-line ignores are TOML-backed suppression. Path patterns and source-line regexes select where findings are ignored; the same suppression also governs safe fixes. Mixed YAML repositories need targeted exceptions without maintaining separate configs or suppressing safe fixes inconsistently. Per-file, per-line
Markdown linting is opt-in. YAML and Markdown are separate source kinds; each embedded YAML region is checked independently. Markdown contains YAML only in selected regions; opting in avoids scanning unrelated prose and file-shape rules. Decision
YAML config aliases have a bound. The loader rejects expansion that would amplify a hostile config into an unbounded graph. A short alias bomb could otherwise expand to billions of config nodes and exhaust memory. Hardening
Migration does not replace an active config. A TOML config shadowing legacy YAML leaves that inactive YAML source untouched. Rewriting shadowed YAML could produce a TOML file that changes which config is active. Decision
Migration flattens relative extends. It checks the config directory before the current directory; a duplicate filename can therefore select different content from lint time. Flattening inheritance preserves a standalone TOML result; the config directory gives relative paths a stable migration base. Review decision
User-global migration refuses relative ignore-from-file. Its meaning depends on each linted file's directory, so one relocated TOML config cannot preserve it. A relative ignore path depends on the linted file location, which a relocated user-global config cannot preserve. Decision, guide
Migration preserves disabled formatting intent. A legacy rule that did not enforce a layout choice becomes an appropriate preserve target, avoiding a new formatting change. Converting an unenforced legacy rule to an active formatter target would introduce an unwanted layout change. Decision
Top-level TOML file exclusion uses exclude. ignore and ignore-from-file are deprecated there, while yamllint YAML and per-rule ignore spellings retain their meanings. The new name distinguishes whole-file exclusion from per-rule ignores while migration warnings preserve old configs. Decision
Path matching follows the platform. Configured paths compare ASCII case-insensitively on Windows and macOS and case-sensitively on Linux. Matching normal platform path conventions avoids missed globs and config changes on Windows and macOS. Decision
Documentation examples use the real config loader. CI validates examples through finalized configuration so misspelled rules and options fail. The finalized loader is the source of truth, so CI should catch examples with unusable rule or option names. Decision

Rules

Decision Why Source
Rules report every problem they find, even where yamllint reports fewer. Fail loudly: a problem ryl detects is reported, never hidden. Complete diagnostics matter more than matching yamllint's output count. Maintainer decision
Built-in rules cover general YAML with evidence. Application-specific schemas, unsubstantiated best practices, and contested style remain outside the rule set. Rules should apply across YAML use cases and have evidence beyond a disputed style preference. Scope, rules epic
Ryl-only rules are TOML-only and opt-in. Merge keys, tags, Unicode line breaks, and block-scalar chomping address portability or clarity without changing yamllint YAML config semantics. These portability and clarity checks have no yamllint counterpart; TOML opt-in avoids changing YAML compatibility. Merge keys, tags, line breaks, chomping
Inline disable directives are shared across rules. # ryl is preferred; # yamllint remains accepted for migration. One engine-level implementation avoids inconsistent rule behavior while retaining existing yamllint directives. Decision
Canonical duplicate-key checks are opt-in. YAML 1.2 core-schema identity can flag keys yamllint treats as different spellings, so TOML enables that semantic mode explicitly. Canonical identity can report duplicates that lexical yamllint checks miss, so users must choose the stricter mode. Decision
Tag allowlists use source spelling. A user's non-core tag pattern matches what the author wrote, while unsafe-tag checks use the resolved identity. Authors need to allowlist the tag spelling they wrote, while unsafe tags must not evade checks through alternate spelling. Decision
Quoted-string checks separate keys from values. Mapping values are checked by default; rewriting keys requires check-keys = true. The default follows yamllint, which checks values; users can opt into rewriting mapping keys. Decision
A single-quote preference can permit necessary escapes. The double-quote exception is an opt-in TOML setting because some strings need escapes a single quote cannot express. Escapes available only in double quotes can make a strict single-quote preference impossible for some values. Decision
Truthy fixes do not guess intent. A safe fix changes only case variants of the same YAML boolean, never yes, no, on, or off. Re-casing an existing boolean preserves its value; rewriting YAML 1.1 words would assume the author’s intent. Decision
Key ordering remains a lint rule. Its safe fix carries attached comments with a block mapping entry and leaves ambiguous cases unchanged. Moving a key without its comments can attach commentary to the wrong entry; ambiguous mappings must remain unchanged. Decision
Key orders may depend on file and mapping. TOML can place listed keys first and choose whether unlisted keys sort or stay in place. Different files and mapping paths need different key orders, and unlisted keys need an explicit policy. Decision
Layout diagnostics stay in ryl check. The formatter also enforces layout and warns if an enabled lint tolerance rejects its target. Keeping layout findings in check follows Ruff and lets users lint without running the formatter. Maintainer re-review, Re-review

Formatter

Decision Why Source
Formatting uses local span edits, never whole-document reprinting. Resolved values, comments, anchors, and untouched bytes are protected; unsafe changes are reported and skipped. Whole-document printers can change values, comments, anchors, and unrelated bytes; local edits support the preservation guarantee. Formatter epic, scope
Safe fixing promises soundness, not completeness. A fix may leave a diagnostic behind; an ambiguous or unparsable region stays unchanged with a reason. A guessed fix can change meaning or turn malformed input into misleading output; leaving a finding is safer. Decision
Meaning-changing repairs need explicit assumptions. Default --fix preserves resolved values under a document's declared YAML version; an unsafe uplift would require opt-in configuration. A safe fix must preserve the resolved value under the document’s declared version; other repairs depend on user assumptions. Decision
ryl format has its own pass list. Formatter targets do not inherit lint rule selection, fixability settings, or lint ignores. Formatting should work independently of which lint rules or ignores a project enables. Decision
The formatter has built-in, overridable targets. The linter remains explicit-rule-only; formatting works without a config and normalizes each option to a single target rather than a lint tolerance. A formatter needs a definite target per option and should work before a user writes lint configuration. Epic, profile
Document-start markers default to preserve. Existing marker style remains unless the user requests a new target, replacing the initial add---- default. Adding --- to an existing file by default creates unnecessary changes; preserve matches formatter precedent. Decision
Unset indent width uses whole-file evidence. Among widths 2–8, the formatter chooses the one changing the fewest lines; ties or no evidence use 2. Whole-file evidence minimizes changed lines; a deterministic fallback resolves ties and files without evidence. Maintainer decision, Maintainer decision
Line-ending auto uses the whole file. The majority ending wins, a tie falls back to LF, and explicit LF remains the default. The majority ending minimizes edits when auto is requested; LF remains the explicit default. Maintainer decision, Maintainer decision
Collection style has flat targets. [format] can preserve, block-format, or flow-format sequences and mappings, converting only cases safe for their comments and values. Collection presentation is layout owned by format; conversions must preserve values and comments. Decision
Quoting preserves cross-version values. Quotes remain when removing them would change resolution, including version-sensitive y and n spellings. Removing a quote can change how YAML 1.1 or other common loaders resolve a scalar. Decision, correction
Markdown boundaries can veto one region. Formatting refuses an edit that would close a YAML fence or front matter early, while continuing with safe neighboring regions. A changed delimiter could end a Markdown region early and turn YAML content into host text. Decision
Formatter guarantees have two gates. Property tests check preservation and idempotence, and a pinned real-repository corpus blocks release on value change, panic, or comment loss. Generated cases prove known invariants; a real corpus exposes patterns the generators missed. Property suite, corpus

CLI and exit codes

Decision Why Source
ryl check names linting. Bare ryl <paths> still lints with a deprecation warning, giving existing callers time to migrate. An additive check command gave callers time to migrate before the formatter split changed the bare command. Command, warning
ryl format writes by default. --check and --diff inspect without writing and exit 1 when formatting is pending; stdin formatting writes to stdout. Writing by default follows formatter command convention; inspection modes let CI detect pending changes safely. Decision
check --diff is a patch preview. It leaves files alone, prints an applicable unified diff where possible, and exits 1 when a safe fix would change input. Ruff-style patch previews support CI review and runners that apply a diff themselves. Decision
- reads stdin with an optional filename. --stdin-filename supplies the diagnostic label, config discovery context, and path-filter context. Editors and pipelines need to lint unsaved YAML; a filename restores file-based config and filtering context. Decision
Markdown fixes splice back into the host. Each embedded YAML region is fixed separately, retaining surrounding Markdown and rejecting boundary-breaking replacements. Splicing isolated regions preserves the Markdown host and prevents file-shape fixes from escaping a fragment. Decision
Symlinks are readable inputs but not fix destinations. Ryl lints a symlinked file and refuses to write a safe fix through its symlink. Following a symlink during --fix could rewrite an unintended target in an untrusted tree. Decision
Input de-duplication is lexical. Absolute paths are normalized without resolving symlinks, so distinct symlink paths remain distinct inputs. Resolving symlinks would merge a symlink with its target and undermine symlink-aware fix handling. Decision, Review rationale
Output destinations are repeatable pairs. CLI format and file targets override [output], which overrides the console default; conflicting destinations fail with exit 2. One run needs both console diagnostics and report files; paired targets make each destination explicit. Decision
GitLab fingerprints exclude location. They hash path, rule, and message so a finding keeps its identity when line numbers move. A location-free fingerprint remains stable when unrelated edits move a finding to another line. Decision
Safe fixes converge in one invocation. Ryl repeats the fix pipeline to a fixed point, with a bounded pass count and a visible non-convergence error. Interacting fixes can expose later fixes, while a cap and visible error prevent an endless loop. Decision
Encoding does not make a pending change invisible. BOM and UTF-16/32 inputs preserve their encoding through formatting; --check and check --diff still report changes when a byte patch cannot be shown. An encoding that cannot carry an applicable text patch must still produce an accurate pending-change exit code. Format, check
A write failure does not stop later files. Formatting reports failed destinations, continues with the rest, and exits 2. One unwritable file should not hide changes or failures in later inputs. Decision
Hook order gives formatting the final layout pass. Run check --fix before format when both pre-commit hooks are enabled. The formatter should make the final layout choice when both hooks rewrite files. Decision

Parser authority

Decision Why Source
One parser stack drives ryl. Granit supplies YAML events; a small local DOM supports configuration without maintaining a second parser implementation. A single event parser avoids conflicting parse results and an unnecessary second parser dependency. Decision
YAML 1.2.2 grammar and the reference parser settle disagreements. PyYAML and ruamel cannot outvote the reference event stream; eemeli's yaml is a cross-check. The reference parser implements the spec event layer; the EOF keep-scalar case shows PyYAML and ruamel disagree with it even together. Decision, EOF evidence, parser comparison
Resolution uses the YAML 1.2 core schema. Plain 011 and 11 are the same integer key, replacing a proposed JSON-schema string reading. One rule using JSON-schema identity would conflict with the core-schema resolver used elsewhere. Core decision, wide integers, Maintainer rationale
Each document's %YAML directive matters. Unversioned input uses 1.2; explicit 1.1 keeps version-sensitive values, a higher minor warns, and a higher major errors. Version-sensitive scalars can change meaning across YAML versions, so each document needs its own resolution policy. Decision
Core tags resolve across spellings. Verbatim and shorthand tags carry the same core-schema meaning in duplicate-key checks, quotes, and configuration. Equivalent core tags must resolve alike regardless of handle or verbatim spelling. Decision
Bare CR is a YAML line break. Rules and safe fixes share YAML 1.2's treatment instead of letting byte scanners disagree. YAML 1.2 defines bare CR as a line break; separate byte scanners had disagreed with parser positions. Decision
Documented divergences stay deliberate. Ryl checks comments after block-scalar headers even where yamllint does not, because the comment is still part of the author's YAML source. The spec makes a block-scalar header comment part of the YAML source, even though yamllint misses it. Decision

Packaging and release

Decision Why Source
A public GitHub release is the final publication step. Registry publishing must succeed first, replacing the earlier order that exposed the release before package feeds were ready. A visible release before registry jobs finish can advertise binaries before package feeds are ready. Current decision, replaced order
Release archives carry verifiable provenance. Publish checksums and GitHub attestations with binaries so users can check an artifact against the release. One checksum list and attestations let downstream installers verify the published binaries. Attestations, checksums
npm uses platform packages. The @owenlamont/ryl package resolves a platform-specific binary instead of downloading one at installation time. Not recorded Decision
Conda-forge builds from source. Its feedstock follows staged-recipes and receives subsequent version bumps from the conda-forge bot. Building from source on conda-forge covers platforms beyond ryl’s prebuilt binary matrix with little ongoing upkeep. Decision
Winget follows a finalized release. The release workflow submits x64 and arm64 archive URLs through wingetcreate. Draft release assets are not publicly downloadable, but wingetcreate needs their URLs to calculate hashes. Decision
Winget fork sync failures fail the release job. A dedicated fork-scoped token replaced the earlier warning-only treatment after a release submission failed. A failed fork sync had blocked submission; the existing token lacked workflow permission without an overly broad grant. Current decision, replaced warning
Downstream editor pins update after release finalization. The release dispatch notifies ryl-vscode once the public release exists. The editor bundles a pinned binary and had fallen behind because release completion sent it no signal. Decision
The changelog supplies release notes. Published notes come from the matching version section rather than ad hoc text, including on draft reruns. Versioned changelog sections provide one stable source for release text, including workflow reruns. Decision
Coverage is earned by named examples. CI omits random property cases from coverage and replays saved seeds separately; local property runs retain their default counts. Random examples can cover regions without a named regression test; saved seeds still replay separately. Skipping random cases also keeps CI runtime down on GitHub-hosted runners. Decision

LSP and editor

Decision Why Source
ryl server reuses the check engine. The default-on LSP feature is a thin protocol adapter; it complements the YAML schema server in an editor. Reusing the check engine keeps editor and CLI results aligned while a schema server handles complementary features. Decision
A client receives one diagnostics channel. Pull-capable clients get pull diagnostics; push is used only when the client lacks pull support, avoiding duplicates. Clients supporting both channels otherwise display each finding twice. Decision
Unchanged workspace pulls wait for a change. Result IDs report unchanged files, and a fully idle workspace request waits for an edit, watched-file event, or config change. Immediate idle replies caused the client to rescan every two seconds; waiting avoids repeated full walks. Decision
Format Document uses ryl format. Editor formatting follows the formatter even without enabled lint rules; Fix All stays a lint-fix action. Editor formatting must use formatter settings even with no enabled lint rules, while Fix All applies lint fixes. Decision
Conflict warnings appear only for explicit changes. Formatting and Fix All may warn about clashing formatter and lint settings; diagnostics and quick fixes remain quiet. Warnings belong to an explicit rewrite request; diagnostics and lightbulb queries should stay quiet. Decision