Skip to content

feat(config): a conformance corpus for resolution - #875

Merged
jdx merged 1 commit into
agent/config-warning-kindsfrom
agent/config-corpus
Aug 14, 2026
Merged

feat(config): a conformance corpus for resolution#875
jdx merged 1 commit into
agent/config-warning-kindsfrom
agent/config-corpus

Conversation

@jdx

@jdx jdx commented Aug 14, 2026

Copy link
Copy Markdown
Owner

The argv corpus pins which token becomes which flag. This pins what a resolution is: given a
registry and layers that supply values, which value wins, where it is recorded as coming from, and
what the merge had to say about the ones it refused.

46 vectors in six sections — precedence, merging, scope, renames, types, choices — each with a
sentence saying what it establishes, because a vector nobody can read is a vector nobody will
maintain.

A registry, not a spec

An argv vector carries a KDL spec, because parsing a command line is a question about a spec. A
resolution is not: it is a question about a registry — keys, types, defaults, merge policies — which
a CLI's build step produces long before anything is resolved. So a vector describes the registry
directly:

{
  "id": "cli-beats-env-beats-file",
  "doc": "The declared order decides: the command line, then the environment, then a file.",
  "settings": [{ "key": "jobs", "type": "uint", "default": 4 }],
  "layers": [
    { "source": "cli", "id": "--jobs", "values": { "jobs": "8" } },
    { "source": "env", "id": "EX_JOBS", "values": { "jobs": "6" } },
    { "source": "file", "id": "hk.toml", "values": { "jobs": "2" } }
  ],
  "expect": { "values": { "jobs": 8 }, "origins": { "jobs": "--jobs" } }
}

An implementation in Go or TypeScript can run this without first acquiring a KDL parser, and how a
spec becomes a registry stays usage-config-build's question, which its own golden test answers.

Warnings are pinned by kind, never by messageout-of-scope, renamed, wrong-type — which
is why #873 comes first. Wording is a quality-of-implementation concern and is expected to differ,
the same line the argv corpus holds for its error codes.

Nothing here touches the world. No file is opened, no environment read, no process started: a
layer is a description of what it supplies, which is all the merge ever sees of one. So no vector's
result can depend on the machine running it.

The harness reads the type grammar itself rather than asking the crate under test. A conformance
harness that asked the implementation what list<string> means would be checking that it agrees
with itself.

What it caught while being written

union on a list keeps duplicates and only a set drops them — I had written the vector the
other way round. The implementation is right (that distinction is what the two types are for), and
there are now two vectors saying so, one per type.

Verification

Four mutations of the runtime, each failing the vectors that describe it rather than a test written
beside it: the scope check letting everything through (4 vectors), union forgetting the lower layer
(4), the boolean spellings narrowed to true (3), and choices unenforced (2). Corpus
well-formedness — unique ids, a doc on every vector, no empty sections — is checked by the same test
file.

No divergence field

The argv corpus records where usage-lib disagrees with the grammar, because that grammar predates it
and has two implementations. Resolution has one, so a disagreement is a bug to fix rather than a fact
to record — and every vector applies to any implementation, since there is no layer of a resolution
that can be left to somebody else the way a binding-only argv parser leaves required to the layer
above it.

Stacked on #873, which adds the warning kinds this pins.

AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5; version: unavailable.


Note

Low Risk
Test-only and corpus additions; resolution behavior is exercised but production resolver code is not changed in this diff.

Overview
Adds a config resolution conformance corpus parallel to the existing argv JSON corpus: 46 KDL vectors in corpus/config/ (precedence, merging, scope, renames, types, choices) that pin registry + layered inputs → resolved values, origins, and warning kinds (not message text).

usage-conformance gains a config module (~850 lines): KDL parse/load, in-memory Layer stubs (no real env/files), a standalone type grammar for building registries, and run/matches against usage-config::resolve. Integration tests in conformance/tests/config.rs run every vector and enforce corpus shape (unique ids, etc.). load_as<T> generalizes corpus directory loading; argv load delegates to it.

Docs: corpus/config/README.md, link from corpus/README.md. Depends on kdl and usage-config in the conformance crate.

Reviewed by Cursor Bugbot for commit 6caadeb. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: d20ec4b5-bbca-4859-bf76-0443d8c9b826

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@jdx
jdx force-pushed the agent/config-corpus branch from a22d8ac to d24a107 Compare August 14, 2026 02:11
@greptile-apps

greptile-apps Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds a language-neutral configuration-resolution conformance corpus and an in-memory Rust harness for validating precedence, merging, scope, renames, types, choices, origins, and warning kinds.

  • Adds 46 KDL resolution vectors across six sections.
  • Builds synthetic registries and layers without filesystem, environment, or process access.
  • Adds corpus integrity checks and rejects integer values unsupported by the harness model.
  • Refactors the existing corpus loader to support generic deserialization while preserving the argv loader API.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
conformance/src/config.rs Adds the KDL parser, registry/layer adapter, resolver runner, comparison logic, and explicit numeric-range validation; the prior integer-conversion findings are fixed at the current head.
conformance/tests/config.rs Runs every resolution vector, validates corpus structure, and exercises unsupported integers through both default and shaped-layer paths.
conformance/src/lib.rs Generalizes JSON corpus loading through load_as while retaining the existing load API and corpus-directory behavior.
corpus/config/01-precedence.kdl Defines precedence and unknown-setting conformance vectors.
corpus/config/02-merging.kdl Defines replace, union, set, and deep-merge conformance vectors.
corpus/config/03-scope.kdl Defines trust and scope-boundary conformance vectors.
corpus/config/04-renames.kdl Defines renamed and deprecated setting behavior.
corpus/config/05-types.kdl Defines scalar, collection, parser, and wrong-type resolution behavior.
corpus/config/06-choices.kdl Defines allowed-choice validation behavior.

Reviews (6): Last reviewed commit: "feat(config): a conformance corpus for r..." | Re-trigger Greptile

Comment thread conformance/src/config.rs Outdated
@jdx
jdx force-pushed the agent/config-corpus branch from d24a107 to 120e797 Compare August 14, 2026 02:16

jdx commented Aug 14, 2026

Copy link
Copy Markdown
Owner Author

Real, and in a conformance harness it is the worst class of bug: as_i64().unwrap_or_default() turned
a number too large for the value model into zero, so the vector went on to pass or fail for a
reason that has nothing to do with what it says. A harness that quietly misreads its own corpus is
worse than one that cannot read it.

Reading a number is fallible now, and the failure reaches both roads a number arrives by — a declared
default or a choice (through registry_of, which already returned a Result) and a value a layer
supplies (through LayerError::Unreadable, naming the key and the layer). The run stops instead of
resolving something nobody wrote.

Test: a vector with 9223372036854775808 as a default, and the same number supplied by a layer.
Mutation: zeroing it again fails both halves.

AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5; version: unavailable.

@jdx
jdx force-pushed the agent/config-corpus branch from 120e797 to 3af6000 Compare August 14, 2026 02:39
@github-actions

github-actions Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Instruction counts

benchmark trend instructions Δ wall (min) Δ
markdown ▂▂▂▃▁███ 175,805,549 → 175,807,509 +0.00% 16.23 → 16.72ms +2.99%
startup ▆█▆▆▆▇▁▄ 1,221,823 → 1,221,953 +0.01% 0.96 → 1.04ms +8.44%

No instruction-count regression above 1%.

Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run.

Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.

Shadow comparison

Parsing mise use -g node@20 against a shadow of mise's committed spec.
Reported, not gated: the shadow grows as the derive learns to express more, so
what to watch is the ratio rather than either column.

usage clap ratio
instructions, cold parse 29956 5895173 196x
usage: argv -> struct                             897 ns      0.90 µs
clap: build tree + parse -> struct             497315 ns    497.31 µs
clap: parse -> struct, tree reused              23721 ns     23.72 µs
clap: build tree only                          302987 ns    302.99 µs

6caadebcdb87 vs cd2f2c7933dc · measured on the runner, not pushed to the history.

stack merge was automatically disabled August 14, 2026 12:08

Pull Request is not mergeable

stack merge was automatically disabled August 14, 2026 12:08

Pull Request is not mergeable

stack merge was automatically disabled August 14, 2026 12:08

Pull Request is not mergeable

stack merge was automatically disabled August 14, 2026 12:09

Pull Request is not mergeable

stack merge was automatically disabled August 14, 2026 12:15

Pull Request is not mergeable

stack merge was automatically disabled August 14, 2026 12:16

Pull Request is not mergeable

stack merge was automatically disabled August 14, 2026 12:16

Pull Request is not mergeable

@jdx
jdx force-pushed the agent/config-corpus branch from 3af6000 to f6b19bb Compare August 14, 2026 12:18
Comment thread conformance/src/config.rs
@jdx
jdx force-pushed the agent/config-corpus branch from f6b19bb to a07eacd Compare August 14, 2026 12:25
The argv corpus pins which token becomes which flag. This pins what a resolution
is: given a registry and layers that supply values, which value wins, where it is
recorded as coming from, and what the merge had to say about the ones it refused.

46 vectors in six sections — precedence, merging, scope, renames, types, choices —
each with a sentence saying what it establishes, because a vector nobody can read
is a vector nobody will maintain.

A vector describes a **registry** rather than a spec. An argv vector carries KDL
because parsing a command line is a question about a spec; a resolution is a
question about keys, types, defaults and merge policies, which a build step
produces long before anything resolves. So an implementation in Go or TypeScript
can run this without first acquiring a KDL parser, and the spec-to-registry
mapping stays `usage-config-build`'s question, which its own tests answer.

Warnings are pinned by kind and never by message, the same line the argv corpus
holds for its error codes. Nothing here opens a file, reads an environment, or
starts a process: a layer is a description of what it supplies, which is all the
merge ever sees of one.

The harness reads the type grammar itself rather than asking the crate under
test, since a conformance harness that asks the implementation what a type means
is checking that it agrees with itself.
@jdx
jdx force-pushed the agent/config-corpus branch from a07eacd to 6caadeb Compare August 14, 2026 12:38

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6caadeb. Configure here.

Comment thread conformance/src/config.rs
.collect::<Result<_, _>>()?,
),
serde_json::Value::Null => Value::String(String::new()),
serde_json::Value::String(s) => Value::String(s.clone()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Null becomes empty string default

Low Severity

#null is converted to an empty string in both const_of and value_of, so a null default is seeded as a real value and a shaped null is supplied as text. That contradicts the comment that null means the key is absent, and it can coerce (for example an empty string is a valid false boolean or an empty list) instead of omitting the setting.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6caadeb. Configure here.

@jdx
jdx merged commit 46e22e0 into main Aug 14, 2026
9 checks passed
@jdx
jdx deleted the agent/config-corpus branch August 14, 2026 12:47
jdx added a commit that referenced this pull request Aug 15, 2026
The [`config` block
reference](https://usage.jdx.dev/spec/reference/config) says how a CLI
*declares*
settings. What happens to them was written down nowhere — precedence,
coercion, scope, renames,
warnings existed only as the merge's implementation and the corpus that
pins it. Which is why the
corpus README I wrote in #875 had to say, in as many words, "these
vectors are the definition, which
is the wrong way round".

Now it is prose first and vectors second, the way `/spec/argv` and the
argv corpus sit together, and
the page is normative rather than descriptive: an implementation in Go
or TypeScript has something to
read before it starts, instead of reverse-engineering 46 JSON vectors.

It states the things a reader would otherwise have to infer from a test:

- a declared default is the **bottom layer**, not a floor applied
afterwards — which is why a `union`
  list with a default and one file gets both
- `union` concatenates, so a `list` keeps duplicates and a `set` drops
them, because that is what
  distinguishes the two
- an empty value **is** a value: `HK_EXCLUDE=` is how a default gets
turned off
- a value the type cannot read costs its own key and nothing else
- scope asks how far a place is **trusted**, not what kind of place it
is, so a pkl file in a checkout
  is as much a thing a repository carries as a TOML file
- an origin names the place (`MISE_JOBS`, `--jobs`, `./hk.toml`) and
never the kind, because a user who
  wants a different answer needs to know what to unset

The corpus README now points at it instead of standing in for it, and it
is in the sidebar under the
argv grammar.

*AI-assisted — Tool: Claude Code; model: anthropic/claude-opus-5;
version: unavailable.*

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Low Risk**
> Documentation and conformance vectors only; no resolver or library
code changes in the diff.
> 
> **Overview**
> Adds a normative **config resolution** spec at
`docs/spec/resolution.md` (precedence, merging, types, scope, renames,
warnings, corpus relationship) and links it from the VitePress sidebar
next to the argv grammar.
> 
> Updates `corpus/config/README.md` so vectors are explicitly secondary
to that prose. **Corpus** gains coverage for multi-hop rename chains
(`threads` → `concurrency` → `jobs`), expanded boolean env spellings
(`y`, `on`, `n`, empty → false), and `set_by_comma` on `set<string>`
with deduplication.
> 
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
216d124. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant