# Why seniority

> seniority against cosmiconfig, lilconfig, dotenv and rc, one capability per row, every cell linked to the test, grade or source that proves it — including YAML, which it deliberately leaves to a loader you pass.

Source: https://seniority.interlace.tools/docs/why-seniority

cosmiconfig and lilconfig find a config file. dotenv loads a `.env`. rc merges files,
variables and argv. Each one answers part of "what is this option set to?". seniority answers
all of it, in one declared order, and adds the part none of them has: **where every value came
from, and what it beat.** Its drop-in paths are graded by each incumbent's own suite. It has no
dependencies.

Every mark links to its evidence. For ours, that is a test in this repository. For theirs, it
is the published source of the exact version compat-oracle grades, or that package's own test
suite. None of the four incumbents is installed in this repository at its graded version, so
their cells link the file on jsDelivr. `scripts/capabilities-lock.test.ts` fails the build when
a cited test no longer contains the title it is cited for, or a link is not pinned to a version.

✓ yes · ◐ partial (what is missing is said) · ✗ no · — does not apply. Every cell links to its evidence: our test or grade, or the incumbent’s source at the version compat-oracle grades.

### Precedence and provenance

| Capability | **seniority** | cosmiconfig | lilconfig | dotenv | rc |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **One precedence: flag, env, config, package.json, default** — `resolve()` merges every source a CLI reads in one declared order, `ORDER`, so the same option cannot be decided differently in two places. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/seniority/src/precedence.test.ts) | [✗ finds and loads one config file or package.json field; merging it with flags and the environment is the caller's](https://cdn.jsdelivr.net/npm/cosmiconfig@10.0.1/dist/Explorer.js) | [✗ finds and loads one config file or package.json field; merging it with flags and the environment is the caller's](https://cdn.jsdelivr.net/npm/lilconfig@3.1.3/src/index.js) | [✗ one layer: a `.env` file under the variables already set, or over them with `override`](https://cdn.jsdelivr.net/npm/dotenv@17.4.2/lib/main.js) | [✓ defaults, then rc files, then `<name>_` variables, then argv](https://cdn.jsdelivr.net/npm/rc@1.2.8/index.js) |
| **Provenance for every value: `--explain`** — Each resolved value says which source set it, where (file and line, or the variable), and every candidate it beat, so "why is this set?" has an answer. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/seniority/src/explain.test.ts) | [◐ the result names the file (`filepath`); not which key came from where, or what it overrode](https://cdn.jsdelivr.net/npm/cosmiconfig@10.0.1/dist/Explorer.js) | [◐ the result names the file (`filepath`); not which key came from where, or what it overrode](https://cdn.jsdelivr.net/npm/lilconfig@3.1.3/src/index.js) | [✗ records nothing about which of `.env` and the environment set a variable](https://cdn.jsdelivr.net/npm/dotenv@17.4.2/lib/main.js) | [◐ `configs` lists the files read; not which source set each value](https://cdn.jsdelivr.net/npm/rc@1.2.8/index.js) |
| **A pure `resolve`: the environment is an argument** — `resolve(specs, layers)` reads no file and no `process.env`, so the same inputs give the same answer anywhere, and a test passes the environment instead of editing it. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/seniority/src/shape.test.ts) | [— has no precedence step; it loads files, which is `discover`'s job here too](https://cdn.jsdelivr.net/npm/cosmiconfig@10.0.1/dist/Explorer.js) | [— has no precedence step; it loads files, which is `discover`'s job here too](https://cdn.jsdelivr.net/npm/lilconfig@3.1.3/src/index.js) | [◐ `parse` and `populate` take their input and target as arguments, and `config()` takes `processEnv`; by default it writes `process.env` and resolves paths against `process.cwd()`](https://cdn.jsdelivr.net/npm/dotenv@17.4.2/lib/main.js) | [✗ reads `process.env` itself, and `process.argv` when no argv is passed](https://cdn.jsdelivr.net/npm/rc@1.2.8/lib/utils.js) |

### Discovery

| Capability | **seniority** | cosmiconfig | lilconfig | dotenv | rc |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **A bounded, symlink-safe find-up** — The upward walk stops at `stopAt`, the filesystem root or a depth limit, whichever comes first, and a symlink ring ends it rather than looping, so a start-up cannot hang on a strange directory. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/seniority/src/search.test.ts) | [◐ stops at `stopDir` (the home directory by default) and does not walk at all unless asked; no depth limit](https://cdn.jsdelivr.net/npm/cosmiconfig@10.0.1/dist/ExplorerBase.js) | [◐ stops at `stopDir` (the home directory by default) or the root; no depth limit](https://cdn.jsdelivr.net/npm/lilconfig@3.1.3/src/index.js) | [— reads `.env` in the working directory, or the paths it is given; it does not search](https://cdn.jsdelivr.net/npm/dotenv@17.4.2/lib/main.js) | [✗ walks from the working directory to the root; nothing stops it sooner](https://cdn.jsdelivr.net/npm/rc@1.2.8/lib/utils.js) |
| **Bring your own format** — Loaders are injected per extension, so a TOML, YAML or JSON5 parser is the caller's choice and the caller's dependency, and an extension with no loader is a usage error naming the option that would supply one. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/seniority/src/load.test.ts) | [✓ `loaders`, over its defaults](https://cdn.jsdelivr.net/npm/cosmiconfig@10.0.1/dist/defaults.js) | [✓ `loaders`, over its defaults](https://cdn.jsdelivr.net/npm/lilconfig@3.1.3/src/index.js) | [— reads the `.env` format only](https://cdn.jsdelivr.net/npm/dotenv@17.4.2/lib/main.js) | [✓ a parser in its fourth argument](https://cdn.jsdelivr.net/npm/rc@1.2.8/index.js) |
| **YAML with no parser supplied** — seniority bundles no format parser: its own loaders have no `.yaml` entry, and the cosmiconfig drop-in reads only the YAML that is also JSON and refuses the rest by name, so a YAML config needs `loaders: { '.yaml': yaml.load }`. | [✗ a product decision, not a gap: no parser dependency for everyone, and cosmiconfig's behaviour exactly with one line](https://github.com/ofri-peretz/burgee/blob/main/packages/seniority/src/load.test.ts) | [✓ through its js-yaml dependency](https://cdn.jsdelivr.net/npm/cosmiconfig@10.0.1/package.json) | [✗ JavaScript and JSON only, unless a loader is given](https://cdn.jsdelivr.net/npm/lilconfig@3.1.3/src/index.js) | [— reads the `.env` format only](https://cdn.jsdelivr.net/npm/dotenv@17.4.2/lib/main.js) | [✗ JSON with comments, or INI](https://cdn.jsdelivr.net/npm/rc@1.2.8/lib/utils.js) |

### Environment variables

| Capability | **seniority** | cosmiconfig | lilconfig | dotenv | rc |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **Env reaches declared options only, by their own names** — A variable sets an option only if the running command declares it, under the prefix in SCREAMING_SNAKE (`MYTOOL_DRY_RUN`), so a stray variable cannot inject a key nobody asked for. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/seniority/src/precedence.test.ts) | [— reads no configuration from the environment](https://cdn.jsdelivr.net/npm/cosmiconfig@10.0.1/dist/Explorer.js) | [— reads no configuration from the environment](https://cdn.jsdelivr.net/npm/lilconfig@3.1.3/src/index.js) | [— loads variables; it maps none of them to options](https://cdn.jsdelivr.net/npm/dotenv@17.4.2/lib/main.js) | [✗ every `<name>_` variable becomes a key, nested on `__`, declared or not](https://cdn.jsdelivr.net/npm/rc@1.2.8/lib/utils.js) |
| **Booleans from env: one spelling each way** — `true`, `1` and `yes` or `false`, `0` and `no` (and nothing else) set a boolean from the environment, and any other value is a CONFIG error naming the fix, rather than a truthy string. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/seniority/src/precedence.test.ts) | [— reads no configuration from the environment](https://cdn.jsdelivr.net/npm/cosmiconfig@10.0.1/dist/Explorer.js) | [— reads no configuration from the environment](https://cdn.jsdelivr.net/npm/lilconfig@3.1.3/src/index.js) | [✗ every value is a string; `FLAG=false` is the truthy string `"false"`](https://cdn.jsdelivr.net/npm/dotenv@17.4.2/lib/main.js) | [✗ every value is a string; `FLAG=false` is the truthy string `"false"`](https://cdn.jsdelivr.net/npm/rc@1.2.8/lib/utils.js) |

### Validation and plugins

| Capability | **seniority** | cosmiconfig | lilconfig | dotenv | rc |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **Every violation names the file, the line and the value** — `validate()` returns every violation at once, each naming where the bad value came from, so a config with three mistakes is fixed in one pass. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/seniority/src/validate.test.ts) | [✗ returns the config as loaded; checking it is the caller's](https://cdn.jsdelivr.net/npm/cosmiconfig@10.0.1/dist/Explorer.js) | [✗ returns the config as loaded; checking it is the caller's](https://cdn.jsdelivr.net/npm/lilconfig@3.1.3/src/index.js) | [✗ returns the variables as parsed](https://cdn.jsdelivr.net/npm/dotenv@17.4.2/lib/main.js) | [✗ returns the merged object](https://cdn.jsdelivr.net/npm/rc@1.2.8/index.js) |
| **Source plugins, ranked between the built-ins** — A plugin adds a source (a vault, a remote config) at a rank between two built-ins, never above the flag the user typed, and `--explain` names it without seniority knowing it. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/seniority/src/plugin.test.ts) | [✗ loaders parse files; there is no source to add](https://cdn.jsdelivr.net/npm/cosmiconfig@10.0.1/dist/index.js) | [✗ loaders parse files; there is no source to add](https://cdn.jsdelivr.net/npm/lilconfig@3.1.3/src/index.js) | [✗ reads `.env` files and its encrypted `.env.vault`](https://cdn.jsdelivr.net/npm/dotenv@17.4.2/lib/main.js) | [✗ a fixed list of files](https://cdn.jsdelivr.net/npm/rc@1.2.8/index.js) |
| **Zero runtime dependencies** — seniority installs one package; rc brings four, and cosmiconfig brings js-yaml and env-paths. | [✓](https://github.com/ofri-peretz/burgee/blob/main/packages/seniority/src/shape.test.ts) | [✗ env-paths and js-yaml](https://cdn.jsdelivr.net/npm/cosmiconfig@10.0.1/package.json) | [✓](https://cdn.jsdelivr.net/npm/lilconfig@3.1.3/package.json) | [✓](https://cdn.jsdelivr.net/npm/dotenv@17.4.2/package.json) | [✗ deep-extend, ini, minimist and strip-json-comments](https://cdn.jsdelivr.net/npm/rc@1.2.8/package.json) |

### Compatibility

| Capability | **seniority** | cosmiconfig | lilconfig | dotenv | rc |
| :-- | :-- | :-- | :-- | :-- | :-- |
| **Passes lilconfig's own test suite** — `seniority/lilconfig` is graded by lilconfig 3.1.3's own tests, all 77, the same count the real lilconfig gets here. | [✓ 77 / 77 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/lilconfig.json) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/cosmiconfig/test/successful-files.test.ts) | [✓ its own suite, the control run](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/lilconfig/src/spec/index.spec.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/dotenv/tests/test-parse-multiline.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/rc/test/test.js) |
| **Passes rc's own test** — `seniority/rc` runs rc 1.2.8's test script, one script of bare assertions with no reporter, and exits 0: the grade is one bit, not 100% of anything. | [✓ 1 / 1 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/rc.json) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/cosmiconfig/test/successful-files.test.ts) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/lilconfig/src/spec/index.spec.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/dotenv/tests/test-parse-multiline.js) | [✓ its own test, the control run](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/rc/test/test.js) |
| **Passes cosmiconfig's own test suite, bar YAML** — `seniority` passes 186 of the 243 cases of cosmiconfig 10.0.1's own suite; 54 of the rest are YAML, which seniority deliberately does not parse without a loader. | [◐ 186 / 243 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/cosmiconfig.json) | [✓ its own suite, the control run; the case the harness cannot load fails for it too](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/cosmiconfig/test/successful-files.test.ts) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/lilconfig/src/spec/index.spec.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/dotenv/tests/test-parse-multiline.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/rc/test/test.js) |
| **Passes dotenv's own test suite, bar the vault** — `seniority/dotenv` passes 106 of the 141 assertions of dotenv 17.4.2's own suite (node-tap counts assertions); the encrypted `.env.vault` and the dotenvx tips are declined, not missing. | [◐ 106 / 141 of its own tests](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/baseline/dotenv.json) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/cosmiconfig/test/successful-files.test.ts) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/lilconfig/src/spec/index.spec.js) | [✓ its own suite, the control run](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/dotenv/tests/test-parse-multiline.js) | [— a different API](https://github.com/ofri-peretz/burgee/blob/main/packages/compat-oracle/vendor/rc/test/test.js) |

## Reading it

- **"ours" is `resolve`, `discover` and the rest of seniority's own API.** The drop-ins keep
  their incumbents' behaviour, which is what their grades measure. `seniority/rc` still turns
  every `<name>_` variable into a key, because rc does.
- **rc has a precedence too**, and the table says so: defaults, files, variables, argv. What
  it does not have is the record of which one won.
- **— does not apply** is not a soft ✗. cosmiconfig reads no environment, so the environment
  rows do not apply to it; the cell says why.

## What it leaves out on purpose

- **YAML.** seniority bundles no format parser; that is a product decision, not a missing
  feature. cosmiconfig's suite grades `seniority` 186 of 243, and 54 of the 57 it misses are
  YAML fixtures. Pass `loaders: { '.yaml': yaml.load }` and you have cosmiconfig's behaviour,
  with the parser as your dependency rather than everyone's.
- **dotenv's vault and dotenvx.** The encrypted `.env.vault`, `decrypt`, and the dotenvx tips
  are declined. They account for 29 of the 35 assertions `seniority/dotenv` does not pass.
- **Rearranging the order.** A precedence a program can reorder is one nobody can reason about
  from outside. A plugin can add a source between two built-ins, never reorder them.

## What is not in the table

- **Weight.** Zero dependencies is a row. The byte figures are on
  [Benchmarks](https://burgee.interlace.tools/docs/benchmarks), measured the same way on both
  sides, because they are figures rather than a yes or no.
- **Speed.** Nothing measures how fast a config is found against lilconfig, so there is no
  speed claim.
- **Symlink handling in the incumbents.** seniority's walk is proven to end on a symlink ring.
  Whether the others loop depends on their filesystem calls, and no test here runs them on one,
  so the find-up row is about the bounds each one has, not about symlinks.
