Resolver Contract
This page records the internal resolver boundary for the dreamcli-re-foundation workstream. It is a stability target for tests and refactors, not a public API guarantee.
Responsibilities
- apply one precedence order to both surfaces:
cli -> stdin -> env -> config -> prompt -> default - read stdin only when resolution will select it
- gate prompts after non-interactive sources have been checked
- coerce and validate sourced values
- collect deprecations from explicit sources
- aggregate validation failures into one thrown
ValidationError
Non-Responsibilities
- read from runtime globals like
process.envor terminal APIs directly - discover config files or package metadata
- parse argv tokens into
ParseResult - run middleware or action handlers
- format terminal-facing error output
Invocation Boundary
The resolver contract is modeled in src/core/resolve/contracts.ts as:
import type {
CommandSchema,
DeprecationWarning,
ParseResult,
PromptEngine,
} from '@kjanat/dreamcli';
interface ResolverInvocation {
readonly schema: CommandSchema;
readonly parsed: ParseResult;
readonly options?: ResolveOptions;
}
interface ResolveOptions {
readonly stdinData?: string | null;
readonly env?: Readonly<
Record<string, string | undefined>
>;
readonly config?: Readonly<Record<string, unknown>>;
readonly prompter?: PromptEngine;
}
interface ResolveResult {
readonly flags: Readonly<Record<string, unknown>>;
readonly args: Readonly<Record<string, unknown>>;
readonly deprecations: readonly DeprecationWarning[];
}The intent is simple:
- callers inject all external state through
ResolveOptions - resolver output is resolved values plus structured deprecation facts
- the executor layer owns rendering and handler execution after this point
Source And Precedence Facts
schema/source.ts names the one stable precedence order, and contracts.ts re-exports it:
const RESOLUTION_ORDER = [
'cli',
'stdin',
'env',
'config',
'prompt',
'default',
] as const;Both surfaces walk it. An explicit - is CLI-sourced with bytes from stdin and keeps CLI precedence, so it lands on cli; the stdin stage is the implicit fallback an absent input takes before env. That order is the behavior contract tests target.
Resolution also records which stage produced each value, keyed by input name. The record distinguishes the two ways stdin delivers bytes ({ stage: 'cli', via: 'stdin', trigger: 'dash' } versus { stage: 'stdin', via: 'stdin', trigger: 'fallback' }) and names the binding that fired ({ stage: 'env', envVar }, { stage: 'config', configPath }). It is internal until the provenance surface lands.
Diagnostic Expectations
- env, config, prompt, and stdin failures carry source-aware detail payloads
- hard coercion errors stop later fallback for that same field
- multiple validation failures are thrown as one aggregate error with per-error details
- aggregate validation failures also include per-issue summaries with normalized input labels and source labels when the failing source is known
- missing-value errors remain actionable via source-ordered suggestions
Redesign Boundaries
This contract intentionally freezes behavior before deeper resolver work:
- module splitting can move orchestration, coercion, lookup, and error helpers apart
- aggregated diagnostics can improve, but source-aware details and explicit precedence must remain testable
- aggregate wrappers may change presentation, but they must keep nested per-error payloads plus explicit per-issue summaries for flags and args
- any shared flag/arg property model must preserve the current stage ordering unless a later contract explicitly changes it
Shared Property Model Decision
The current resolver now makes that decision explicit in src/core/schema/value.ts:
- the shared flag/arg value model is coercion-only
- it covers the overlapping kinds
string,number,boolean,enum, andcustom, and a collection reaches it through the value of its element - how many values a source carries belongs to
src/core/schema/cardinality.ts, which owns splitting, aggregation, and declared-default validation - it does not own precedence order, fallback order, prompt/stdin policy, or required-value validation
That split is intentional.
Both surfaces own cli -> stdin -> env -> config -> prompt -> default. The source axis is shared because the sources themselves are the same set; schema/source.ts normalizes a flag or arg schema onto an ordered SourceBinding list, and resolve/stages.ts walks that order once for both. What stays per-surface is what genuinely differs: how a parsed CLI value reads, how a raw value coerces, and how diagnostics name the subject.
Evidence
- Contract module:
src/core/resolve/contracts.ts - Shared value model:
src/core/schema/value.ts - Current implementation:
src/core/resolve/index.ts - Existing behavior tests:
src/core/resolve/*.test.ts - RFC / PRD source:
specs/dreamcli-re-foundation.md,specs/dreamcli-re-foundation-prd.md
Current Status
- resolver input and output boundaries are now named explicitly in code
- precedence and diagnostic expectations now have one internal contract module
- resolver orchestration remains in
src/core/resolve/index.ts, with flag and arg paths delegated toflags.tsandargs.ts