Precedence and --explain
resolve() applies one fixed order — flag, env, config, package.json, default — and records, for every value, the source that set it, the file line, and every candidate it beat, as text, --json data or an agent event.
resolve(specs, layers) takes the options a command declares and the layers a run has. For
every option it returns three things:
| field | what it is |
|---|---|
values[key] | the value that won |
provenance[key] | { source, location, line? }: where it came from |
candidates[key] | every source in ORDER, set or not, with its value |
The first source in ORDER that sets an option wins: flag, env, config, package,
default. flags must hold only what the user typed. A parser's defaults belong in the spec;
if they were passed as flags, they would outrank the environment.
The record behind --explain
explain(name, result) prints the answer a person reads. explanation(name, result) is the
same answer as a record. explanationJson projects it for --json, and explanationEvent
for an agent's event stream:
import { explanation, explanationEvent, explanationJson, resolve } from 'seniority';
const specs = { region: { type: 'string', default: 'us-1' } };
const result = resolve(specs, { flags: {}, env: { APP_REGION: 'eu-3' }, envPrefix: 'APP', config: { path: 'app.config.json', data: { region: 'eu-2' }, lines: { region: 2 } } });
const record = explanation('region', result);
console.log(JSON.stringify(explanationJson(record)));
console.log(JSON.stringify(explanationEvent(record)));{"option":"region","value":"eu-3","source":"env","location":"APP_REGION","candidates":[{"source":"flag","location":"--region","set":false},{"source":"env","location":"APP_REGION","set":true,"value":"eu-3"},{"source":"config","location":"app.config.json","line":2,"set":true,"value":"eu-2"},{"source":"default","location":"default","set":true,"value":"us-1"}]}
{"event":"config.explain","data":{"option":"region","value":"eu-3","source":"env","location":"APP_REGION","candidates":[{"source":"flag","location":"--region","set":false},{"source":"env","location":"APP_REGION","set":true,"value":"eu-3"},{"source":"config","location":"app.config.json","line":2,"set":true,"value":"eu-2"},{"source":"default","location":"default","set":true,"value":"us-1"}]}}An agent gets the same answer a person does: the winner and every candidate. Candidates that
lost and candidates that were never set are told apart by set. A JSON config layer loaded by
discover carries the line each key is set on, so the record points at app.config.json:2,
not just the file. An option the command does not declare is reported as undeclared, not as
unset.
Bring your own option type
resolve reads three fields of a spec and ignores the rest:
type: only'boolean'changes how the environment is read;env;default.
So the option type your CLI already has works as it is, with no adapter. In TypeScript, a
richer type is already an OptionSpec by structure; burgee passes its own eighteen-field
option type straight in.
import { resolve } from 'seniority';
// A CLI's own option shape: seniority reads `type`, `env` and `default`, and nothing else.
const options = {
region: { type: 'string', default: 'us-1', choices: ['us-1', 'eu-2'], describe: 'where to deploy', group: 'Deploy' },
verbose: { type: 'boolean', alias: 'v', describe: 'say more', hidden: false },
};
const { values, provenance } = resolve(options, { flags: { verbose: true }, env: {} });
console.log(values, provenance.region.source, provenance.verbose.source);{ region: 'us-1', verbose: true } default flagWhat is tested
precedence.test.ts:- flag > env > config > package.json > default, each pair;
- the default is last and has no location;
ORDERis the declaration, and the candidates follow it exactly.
explain.test.ts:- the winner and every candidate, in
ORDER; - lost and unset are told apart;
- the line is carried;
- the text is the record rendered, byte for byte;
--jsonand the event are the same record.
- the winner and every candidate, in
Getting started
Install seniority, declare three options, resolve them from flags, the environment, a config file and defaults in one fixed order, and ask where each value came from.
Config files
discover() finds a config file in a fixed order, follows extends, and records each key's line; search() walks upward only when asked, bounded by stopAt, a depth limit and the root; loaders bring any format.