Repository navigation
Value provenance: track resolution stage and expose explicitness to handlers #90
Description
Sub-issue of #88 — Part 1.
Problem
FLAG_RESOLUTION_ORDER = ['cli', 'env', 'config', 'prompt', 'default'] is an ordered, named chain, and the failure path already models sources via FlagDiagnosticSource. On the success path the winning stage is dropped: ResolveResult is {flags, args, deprecations} and ActionParams narrows to {args, flags, ctx, out, meta}. A handler can observe what a flag resolved to, never whether the user said it.
Why it matters
- Mutually exclusive flags — both-passed should be a hard error, not a silent precedence rule.
- Warning on ignored input — "you passed
--x, it has no effect in this mode" is only honest if--xwas actually typed. - Explicit-wins merges over a project manifest — a defaulted value must not silently outrank the manifest.
- Derived or conditional defaults — today the only way to express "unset" is to omit
.default(). --explain-style diagnostics — wants the stage label, not just a boolean.
(1)–(4) need only a boolean; (5) needs the stage.
Every current workaround is lossy: dropping .default() removes defaultValue from the definition document, so --help --json and completions lose it; comparing against the default sentinel fails in exactly the case the check exists for, a user explicitly passing a value equal to the default; and .derive() runs after resolution, so it cannot tell explicit from default either.
Scope
Two layers, shipped together so the plumbing is reachable.
1. Resolver — carry the winning stage onto the success path
interface ResolveResult {
readonly flags: Readonly<Record<string, unknown>>;
readonly args: Readonly<Record<string, unknown>>;
readonly deprecations: readonly DeprecationWarning[];
readonly flagSources: Readonly<Record<string, FlagResolutionStage>>;
readonly argSources: Readonly<Record<string, ArgResolutionStage>>;
}2. Handler surface — expose it on ActionParams
Shape A, full provenance:
.action(({ flags, sources }) => {
if (sources.flags.output === 'cli' && flags['keep-name']) { /* … */ }
});Shape B, narrower predicate — covers (1)–(4) without committing the stage vocabulary to the public API, loses (5):
.action(({ flags, wasExplicit }) => {
if (wasExplicit('output') && flags['keep-name']) { /* … */ }
});Open question: ship A, B, or both.
Acceptance criteria
- Winning stage recorded for every resolved flag and arg
-
FlagResolutionStage/ArgResolutionStageexported from the@kjanat/dreamcliroot — today they stay incore/resolve/contracts - A handler can distinguish explicit from defaulted without dropping
.default() - Declared defaults keep their
defaultValuein the definition document - Docs: revise the Limitations page, where the "Exactly One Of"
.derive()workaround silently depends on this gap not being hit
Blocks the cross-flag constraint work — Part 2 needs Part 1 underneath.
Activity
coderabbitai commentedon Aug 1, 2026
🔗 Related PRs
#1 - feat: shell completions, config discovery, custom flags, deprecation, and nested subcommand trees (v0.5–v0.7) [closed]
#12 - add: prompt-flag compatibility validation [closed]
#57 - feat: themed help output + negatable/duplicates/case-parity flags (rc.8) [merged]
#60 - feat(schema): Standard Schema interop and DSL removal [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!