seniority

Changelog

Every release of seniority, newest first, from its CHANGELOG.md — what changed and the pull request it came from.

0.6.4

Patch Changes

  • #674 e9f45d8 Thanks @ofri-peretz! - README: family header, badges, install, migrating, the family table.

    Every package README now opens the same way — lockup, tagline, one badge row in one order (npm version, downloads, Quality Gate, the package's own coverage, OpenSSF Scorecard, unpacked size, dependencies, types, Node, licence, npm provenance), a row of compatibility badges read from the graded baseline — and carries the same sections in the same order: Install for npm, pnpm, yarn and bun, Quick start, Migrating as a before/after diff, Compatibility, Benchmarks, For agents, API, and a generated table of the nine packages. Links are absolute, so they work on npm as well as GitHub.

  • #731 bc68493 Thanks @ofri-peretz! - seniority/cosmiconfig now matches cosmiconfig 10.0.1 in three places where it did not.

    • stopDir: '' searches the start directory alone, as upstream's truthiness check does. Before, an empty stopDir switched the search to global and walked up to the working directory and then into the global config directory.
    • packageProp walks a path the way upstream does. A path through a string reads the string's own properties, so 'name.length' is a number. A path through a null, such as "foo": null under packageProp: 'foo.bar', throws the TypeError upstream throws, annotated with the file. Before, both answered "not found", and the search moved on to the next file.
    • A start directory that cannot be stated for any reason but absence rejects the search with the stat error, as upstream's isDirectory does. Before, every such failure read as "no config here". Which paths fail that way is the platform's call: on Linux and macOS, search('<file>/sub') rejects with ENOTDIR and a directory the process may not enter with EACCES; Windows reports <file>\sub as not found, so there it is still "no config here", as it is upstream.

    loadJson no longer wraps a non-Error in an Error, because JSON.parse of a string throws nothing else.

  • #732 ee6780b Thanks @ofri-peretz! - Code no input could reach is removed. There is no behaviour change.

    • seniority check loses a "(replaces …)" helper it never called, and a map that returned each source name unchanged. It loads one plugin into an emptied registry, so there is nothing for a source to replace. The header now says that.
    • seniority/lilconfig no longer writes dirname(p) || sep, because Node's dirname never returns ''.
    • seniority/dotenv's parser no longer checks that a match has a key, because the key group in its pattern is not optional.
    • seniority/rc's comment stripper reads characters with charAt, so there is no fallback for an index that is always in range.
    • The JSON loader behind seniority/config quotes the parser's own message without first checking that it threw an Error, because JSON.parse of a string throws nothing else.

0.6.3

Patch Changes

  • #627 4a629a9 Thanks @ofri-peretz! - Lint with every published Interlace ESLint plugin, and fix what the upgrade surfaced.

    • caique: the inquirer theme merge skips __proto__, constructor and prototype keys, so a theme object cannot swap the merged object's prototype.
    • burgee: last-element reads use .at(-1).
    • burgee, closeout, flagstaff, roundel: helpers that capture nothing from their enclosing function move to module scope.
    • seniority: suppression comments name the no-dynamic-require rule that now reports the config loader's dynamic require.

    No public API or output changes.

0.6.2

Patch Changes

  • #604 0e7b1e8 Thanks @ofri-peretz! - Each README links to its migration guides under the docs link: "Migrating from: chalk", "ora · log-update · boxen · cli-table3", and so on. That puts a path from the npm page to the guide for the library you are replacing. No code changes.

0.6.1

Patch Changes

  • #588 073037a Thanks @ofri-peretz! - READMEs and package descriptions now match what each drop-in path is graded at. bellpull names bellpull/node-which as the drop-in for npm which (5 / 5) and no longer lists execa as a drop-in. paratext's ansi-escapes row is 4 / 4, with the CSI half implemented. seniority's rc row is 1 / 1, dotenv's config() defaults to process.env, and lilconfig is 77 / 77. linegauge documents ambiguousIsNarrow and strip as shipped. No code changes.

0.6.0

Minor Changes

  • #486 f08ff58 Thanks @ofri-peretz! - The seniority/dotenv and seniority/rc drop-ins now read the process by default, like the packages they replace. config() with no arguments populates process.env from ./.env; rc(name) reads the process environment. config() also accepts a URL or ~/ path, calls fs/os in a way test stubs can intercept, and returns parsed alongside any error, matching dotenv 17. Graded by each incumbent's own test suite: dotenv 80 → 106 of 141, rc 0 → 1 of 1. seniority's resolver still never reads the process itself.

Patch Changes

  • #474 1955419 Thanks @ofri-peretz! - burgee plugins can hook two more stages. parse runs before the command is resolved: it receives argv and may return a replacement, which is how an alias plugin maps d to deploy. shutdown runs once as the program exits, whether the command succeeded or failed. The family schema.json shipped in every package now describes both stages.

  • #522 f4be6a8 Thanks @ofri-peretz! - require('bellpull/cross-spawn'), require('flagstaff/cli-table3'), require('burgee/yargs'), require('seniority/dotenv') and require('seniority/rc') now return what the incumbent's require() does — the function, the class, the factory, the object — instead of an ES module namespace. Each exports its default as 'module.exports', which is what Node hands a CommonJS caller, and which yargs' own entry already does. const spawn = require('…'); spawn(…) threw before.

0.5.1

Patch Changes

  • #508 1aae1e2 Thanks @ofri-peretz! - <package> --help and --version answer instead of crashing. The bin took its first argument as the plugin file to import, so roundel --help failed with Cannot find module '…/--help' and exit 1. -h/--help now print usage and exit 0, -V/--version print the version and exit 0, and any other flag where the plugin file belongs is a usage error, exit 2.

0.5.0

Minor Changes

  • #507 b8e97dc Thanks @ofri-peretz! - Runs on Node 20 and 22, not just 24+: engines.node is now ^20.19.0 || >=22.13.0. Those are the first releases where require(esm) loads without a warning, so the CommonJS require() path keeps working. Every package's test suite runs on exactly 20.19.0 and 22.13.0, on Linux, macOS and Windows. caique's prompts no longer call Promise.withResolvers, which Node 20 doesn't have.

Patch Changes

  • #505 9800b43 Thanks @ofri-peretz! - Docs: the Benchmarks section's weight ceiling is re-measured against a fresh install of each incumbent's latest release (cosmiconfig 10.0.1, slice-ansi 9.0.1, which 7.0.0, dotenv 18.0.3, …) instead of the copies hoisted in this workspace, and names incumbents that were measured but left out of the ceiling as exactly that.

0.4.3

Patch Changes

  • #494 f7f6d4b Thanks @ofri-peretz! - Each package's homepage and README docs link now point at its own documentation site, https://<package>.interlace.tools, instead of a page on burgee's site. The old burgee.interlace.tools/docs/packages/<package> URLs answer with a 301 to the new host, so nothing already linked breaks. closeout's README override example also resolves to the current release again (npm:closeout@^0.4; the 0.4.0 release left it at ^0.3).

0.4.2

Patch Changes

  • #465 acf98f3 Thanks @ofri-peretz! - Each README now opens with the incumbent it replaces and the agent surface it serves (--json, an agent event, or a static projection), so npm shows both above the fold. README text only; no code changed.

0.4.1

Patch Changes

  • #454 b4584e7 Thanks @ofri-peretz! - Every package's npm homepage now points at its page on the docs site, https://burgee.interlace.tools/docs/packages/<name>, and each README links it under the header. The keywords add what people and models search for: burgee gains cli-framework, argument-parser, subcommands, json-schema, mcp-server, model-context-protocol, ai-agent, llm, shell-completion, typescript, zero-dependency, commander-alternative and yargs-alternative; the other eight gain agent, ai-agent, non-tty, json and zero-dependency where the package does that — zero-dependency only on the six that install nothing at all.

    burgee's README gains a short FAQ (commander alternative, agent use, MCP, dependencies) and states the compatibility counts the oracle holds — 1,360 / 1,360 of commander's tests and 804 / 804 of yargs' — where it had said 1,215 and 1,185. caique's README no longer calls a released package pre-release.

  • #442 bdaf364 Thanks @ofri-peretz! - Every package now lists plugin, plugins and extensible in its npm keywords, because every package takes plugins through one shared contract.

    A plugin is a plain object, validated against the schema.json that ships in every package, and checked with the package's own check command. Each package reads its own key and ignores the rest, so one object can extend any subset of the family. The plugins page has a nine-layer example that every package's check accepts in CI.

  • #435 7888524 Thanks @ofri-peretz! - schema.json now describes every plugin host in the family.

    The one schema each package ships as its plugin contract used to cover only four hosts: roundel's tokens, flagstaff's glyphs, spinners, borders and components, paratext's capabilities, and linegauge's widths. Five hosts validated their keys in their own code, but the file an author (or a model) writes against said nothing about them. It now describes all of them:

    • bellpull resolvers, including the absolute-path rule on paths
    • caique widgets
    • closeout handlers, including the phases a plugin may use
    • seniority sources, including the rank bounds
    • burgee commands, hooks and enforce

    Where the schema can express a rule, it gives the same verdict as the host's own validator, and a test holds the two together. Function-valued fields (static, run, read, handler) are described and required, but not typed, because JSON Schema can't say "function".

    flagstaff now validates a plugin against only its own keys, not the whole family schema. It no longer refuses a plugin over another host's key, which lets one plugin object contribute to several hosts. Its entry points are also 4.7–5.9 KB lighter for it.

  • #445 dac303e Thanks @ofri-peretz! - Every package now declares sideEffects truthfully, so bundlers can drop what you don't import.

    Six packages declared nothing, so no bundler could drop any of their modules. A named import from the root now bundles to the same bytes as the same import from its subpath:

    importbeforeafter
    import { explain } from 'seniority'2,939 B1,067 B
    import { decide } from 'caique'1,235 B734 B
    import { strip } from 'linegauge'1,102 B940 B
    import { once } from 'closeout'353 B235 B

    flagstaff and roundel used to declare false, but each ships a check command whose file runs when loaded. Each now lists that file, which is the true statement. paratext also lists the two modules that register its built-in capabilities when they load.

0.4.0

Minor Changes

  • #421 db3c59e Thanks @ofri-peretz! - Every plugin host has a check command.

    npx linegauge check ./my-widths.mjs
    npx burgee check ./my-plugin.mjs --json

    PRINCIPLES 7 asks three things of an extension surface: the plugin is data validated against one published schema, there is a check command that shows it every way it can be seen, and the bar is measured. The first was built in all nine hosts; the second existed in flagstaff alone. So an author writing a plugin for any other host found out what it did by shipping it into a program — and a surface nobody can check is a surface nobody outside this repository can write against.

    Each command validates, registers, and shows what the host does with the plugin, in the host's own terms: linegauge measures each code point before and after the override, paratext shows a capability's encode and its fallback, roundel each token and what it replaced, caique each widget's static projection rendered with its own sample. burgee's returns a document rather than printing one, so burgee check --json is the form an agent that just wrote a plugin reads.

    They share one contract with the author, held identically across all nine:

    • a readable report, contribution by contribution, with ok as the last line;
    • a refusal with a code from the family's vocabulary and a fix, exit 1;
    • E_NO_CONTRIBUTION for a plugin that contributes nothing to this host — the schema allows unknown keys so one object registers everywhere, which makes a misspelled key silent, and this is how that typo tells on itself;
    • exit 2 with no file.

    Each host also gains an eval case measuring the one-turn claim, proved to discriminate before it was committed: green against a correct plugin, red against the same plugin with one field broken.

  • #418 c0fa8a3 Thanks @ofri-peretz! - explain moves from seniority/precedence to seniority/explain.

    A re-export is not free across a package boundary. explain was exported from precedence.ts, so every program that resolved a configuration loaded explain.js whether or not anything ever explained one — 1,018 bundled bytes and one more module on the startup path for the branch taken when a user asks why did this option get that value.

    import { explain } from 'seniority' is unchanged: the root barrel still exports it, from its new home. Only seniority/precedence stops re-exporting it. burgee/config re-exports it the same way it always did, and burgee's engine loads it behind an await import('seniority/explain') on the --explain branch, which is now the only thing that pays for it.

    Measured on burgee's core entry: 28,637 → 27,552 bundled bytes, and 22 → 21 modules for import 'burgee'.

Patch Changes

  • #430 4d1b2b3 Thanks @ofri-peretz! - check now reports every refusal with its code and its fix, wherever it was raised.

    Some plugin files register themselves on import: they call register() at the top of the module and export the result. Until now, when such a file was refused, the error was thrown inside check's import(), before the only try that turns a PluginError into E_PLUGIN_SCHEMA: … plus a fix: line. The author got the bare message on stderr, with no code and no fix. Now the whole of check runs inside that one handler, so every refusal comes out the same way on every host.

0.3.1

Patch Changes

  • #386 5a85175 Thanks @ofri-peretz! - The Unicode segmenter is built on first use rather than at import, and both packages now strip comments from what they publish.

    new Intl.Segmenter() loads ICU's grapheme-break data. Two were constructed at module scope, and almost nothing paid for them: every caller takes the ASCII fast path first, so a run of printable ASCII — a help screen, a flag name, a path — never reaches segment(). segmenter is now a function; the two call sites become segmenter().

    linegauge and seniority were also the two published packages whose build never ran strip-comments at all. Unpacked: linegauge 83,538 → 56,148 and seniority 193,682 → 139,793.

    Together these take import 'burgee' from 56.87 ms to 44.39 ms, medians of seven.

0.3.0

Minor Changes

  • #379 955b979 Thanks @ofri-peretz! - seniority/lilconfig and seniority/rc — two new drop-in subpaths, and seniority/dotenv grows the default export its incumbent has.

    seniority/lilconfig is lilconfig 3.1.3's surface: lilconfig, lilconfigSync, defaultLoaders, defaultLoadersSync, and no fifth runtime export, because lilconfig's own suite compares the module's keys against cosmiconfig's. Graded at 67 / 77 against that suite, up from 0 — the same number its control scores, so no case in it now separates the two. The zero was not a missing feature: the row had been pointed at the package root, which presents cosmiconfig's surface and answers lilconfigSync is not a function seventy-seven times.

    seniority/rc is rc 1.2.8's merge with none of its four dependencies: the file stack in rc's own order, __ nesting for environment keys, JSON-with-comments, deep-extend's merge, and configs / config reporting which files were read. INI is refused by name with the argument that would parse it, the way YAML already is. Its environment arrives as an argument rather than off the process, which is the one divergence and the reason its compat row stays at 0 / 1.

    seniority/dotenv now has a default export carrying config, parse and populate, so a CJS caller require()ing it gets the same mutable object require('dotenv') gives — which is what dotenv's own suite stubs. populate also matches 17.4.2 more closely: it validates parsed (not processEnv), returns the keys it actually set, and logs under debug. The row moves 74 / 141 → 80 / 141.

0.2.0

Minor Changes

  • #316 c8acb28 Thanks @ofri-peretz! - seniority to 1.0's requirement set: cosmiconfig's surface, measured at 186 / 241.

    The root export now carries cosmiconfig's own API — cosmiconfig, cosmiconfigSync, Explorer, ExplorerSync, defaultLoaders, defaultLoadersSync, the search-place and loader tables, decodeFileContent and getPropertyByPath — with all three search strategies, both caches, $import with mergeImportArrays, and the meta-config merge. Graded by cosmiconfig 10.0.1's own suite, run unmodified, it goes from 3 / 241 (1.2%) to 186 / 241 (77.2%) against a control of 240 / 241.

    Every one of the 55 cases it does not pass is one of two named divergences: 54 are the absent YAML parser — this package bundles no format parser, so loadYaml reads the JSON subset of YAML and refuses the rest with an error naming the loaders option that supplies one — and one is a test-harness file path. None is a difference in how a config is found, merged or reported.

    Two new compatibility subpaths:

    • seniority/dotenv — dotenv 17's parse and populate, grammar included. config takes the environment it populates as processEnv rather than reaching for process.env, so nothing in this package touches the process.
    • seniority/find-up — findUp, findUpSync, findUpMultiple, findUpMultipleSync over a bounded, symlink-cycle-safe upward walk, in place of four packages.

    Also new on the root export, all additive:

    • explanation() — --explain as a record, with explain()'s text now literally a rendering of it, alongside explanationJson() and explanationEvent().
    • search() / searchAll() — the walk, bounded by stopAt, a depth limit and the filesystem root.
    • loadPath(), loaderFor(), LoaderError — four builtin loaders, injected loaders for every other format, and a usage-class error naming the extension and the option.
    • validate() / check() — a violation reported with its provenance: `out` must be a string; `./mytool.config.js:3` set it to `4`.
    • provenance.line, recorded per key for JSON config layers, and discover's loaders, extensions, upward and stopAt options.

    No behaviour of the existing API changes, and the package still declares zero dependencies.

  • #294 3f92a60 Thanks @ofri-peretz! - Host sources, the plugin key, at seniority/plugin — a plugin adds a resolution source (a vault, a CI variable set, a remote config) and --explain names it as the provenance of the value it won.

    Source is now an open union, so this is additive rather than a type break, and the one code change it costs is describe()'s default branch: an unknown source renders itself from the source and location it declared, which is how a plugin explains itself without seniority knowing its name.

    ORDER is exported and is the single declaration the union and the new RANK are both generated from — the design's array and the shipped union had disagreed (project/home/pkg against config/package), and a plugin must not register against two spellings. The shipped five win, because provenance.source is a value users already read and because project versus home was one kind with two locations, which location already names precisely.

    A plugin's rank slots its source between two built-ins and is refused outside (flag, default): it can never beat the flag the user typed, nor sink below the declared default. The order stays the fixed thing it claims to be.

Patch Changes

  • #316 c8acb28 Thanks @ofri-peretz! - mergeAll refuses prototype as well as __proto__ and constructor, and the guard is three comparisons rather than a Set lookup.

    The guard existed and had no test — it was written, and believed. CodeQL's js/prototype-polluting-function could not see it through the Set binding and blocked a merge on it, which is a fair complaint about a security guard: one a reader has to follow a binding to find is one a reviewer will miss too.

    What it actually prevents, measured rather than assumed. A first attempt at the test asserted ({}).polluted === undefined after merging a __proto__ key and passed with the guard deleted — target['__proto__'] = v goes through the setter and swaps that object's prototype; it does not write Object.prototype. The damage is narrower and quieter: the config object handed back to the caller silently inherits whatever the file said, so config.isAdmin can answer for a key no file set at the top level. cosmiconfig-util.test.ts now asserts that, and four of its six cases go red when the guard is removed.

  • #339 f295630 Thanks @ofri-peretz! - schema.json constrains token names, because it was promising something no host honours.

    tokens was described as any name to a #rrggbb colour. roundel's validate() accepts ten semantic names — error, warn, ok, hint, muted, command, flag, value, heading, ground — and throws on everything else. So a plugin author doing exactly what their own E_PLUGIN_SCHEMA error tells them, comparing their object against roundel/schema.json, got a green from the schema and "accent" is not a token from register(). Measured 2026-09-16 with { accent: '[#336699](https://github.com/ofri-peretz/burgee/issues/336699)' }.

    The schema now carries propertyNames.enum, and scripts/plugin-contract-lock.test.ts pins the enum and the runtime set to each other from both sides, so neither can grow a name the other does not know.

    Every host ships a byte-identical copy of this file (plugin-schema-lock.test.ts asserts it), which is why nine packages are listed. Only the key roundel owns is constrained: describing widgets, handlers, sources, resolvers or commands in a file all eight hosts share is what made flagstaff start validating caique's key last time (PluginError: plugin.widgets.later: expected object, got boolean), and those stay in plugin-schema-lock's UNDESCRIBED list with that reason.

    linegauge is in the list for a different change: ceilings.json's R9 block now records the bar as D1's tree-inclusive ceiling — 83,538 against 170,342, a ratio of 0.4904 — and keeps the superseded get-east-asian-width bar beside it with the count of entries that cleared it.

0.1.0

Minor Changes

  • #219 ac7b9e5 Thanks @ofri-peretz! - seniority resolves flags, env, config and defaults — with provenance

    The package existed as a reserved name exporting a string constant. It now does the job its description has always claimed.

    flag  >  env  >  config file  >  package.json field  >  default

    resolve(specs, layers) returns the values, the provenance of each, and every candidate that lost — so explain(name, resolution) can print the winning source and the ones it beat, generated by the same code that picked the value. A --explain built any other way can drift from the truth; this one cannot.

    resolve is pure: layers in, values out, no filesystem and no process.env. discover is the half that touches the disk and is a separate import for that reason — searching NAME_CONFIG, ./name.config.{json,mjs,js,cjs} and $XDG_CONFIG_HOME/name/config.json, following extends with deep merge and rejecting cycles with the chain that formed them.

    The code is burgee's, moved down a layer where it belongs: precedence.ts and config.ts with their 28 tests, which passed unchanged. Its OptionSpec here declares only the three fields resolution reads, so any program's richer option type satisfies it structurally — no adapter, no import, and no dependency pointing back up the stack.

    Zero dependencies; Node builtins only.

On this page