From a284f375ff4b4aef40830ea90971d4b1893f8720 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Thu, 2 Apr 2026 21:18:25 -0400 Subject: [PATCH 1/3] Added specs --- specs/configliere-spec.md | 779 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 779 insertions(+) create mode 100644 specs/configliere-spec.md diff --git a/specs/configliere-spec.md b/specs/configliere-spec.md new file mode 100644 index 0000000..5f373b0 --- /dev/null +++ b/specs/configliere-spec.md @@ -0,0 +1,779 @@ +# Configliere Specification + +**Version:** 0.4 — Draft (final sentence-level calibration) +**Status:** Normative draft for repository inclusion +**Derived from:** Reverse-engineering research and focused research briefs (phased parsing, type inference, introspection architecture, environment-variable scoping), grounded in 13 transcript sessions and current library source code. +**Revision note:** This version makes a final sentence-level pass. Source equivalence claims are scoped with SHOULD where source-form limitations apply. Source priority is labeled as provisionally normative. The inject resolve-function pattern is labeled as current API shape. Helper extraction obligation distinguishes the convention (normative) from the roster (current). Test agenda items are annotated by purpose: [Verify current] vs. [Lock down contract]. + +--- + +## 1. Purpose + +Configliere is a parser-combinator configuration system for TypeScript applications that run across CLI, server, and tooling contexts. + +Configliere treats application configuration as a **single pre-validated value** rather than a scattered set of ad hoc reads at runtime. A Configliere program defines a schema of typed fields, composes them into objects and commands, and parses input from three unified sources — CLI arguments, environment variables, and config-file object values — into one validated result with full source provenance. + +The system produces: + +- a typed, validated configuration value, +- a record of which source provided each value and why, +- structurally derived help text, +- and a parser tree that is inspectable at any point. + +Configliere uses Standard Schema validators for field-level validation, making it compatible with arktype, zod, valibot, and other conforming schema libraries. + +--- + +## 2. Design Philosophy + +### 2.1 Source equivalence + +Source equivalence is a core design goal: CLI arguments, environment variables, and config-file object values are treated as conceptually equivalent input channels for *field-level configuration*. A field that accepts `--port 8080` from the CLI SHOULD also accept `PORT=8080` from the environment and `{ port: 8080 }` from a config file. The same schema validates all three. + +Source equivalence applies to the field/configuration model — the space of typed scalar and simple values that fields can represent. It does not imply perfect symmetry across all configuration concepts and source forms: + +- Command *selection* is CLI-only (§9.1). This is an explicit asymmetry, not a gap. +- Env vars undergo string coercion that CLI and config values do not require. +- Env vars have no natural representation for nested objects or arrays beyond simple conventions (e.g., comma-separated lists). +- Not every structural concept (e.g., a command map, a nested object tree) has a direct env-var equivalent. + +Within the supported field model, every field SHOULD be configurable through every source channel, and every source MUST be validated by the same schema. Where a source cannot naturally represent a field's value (e.g., an env var for a deeply nested object), the limitation is inherent to the source form, not a violation of source equivalence. The goal is that a user who knows one source can predict the behavior of another — not that every source is syntactically identical or structurally equivalent. + +### 2.2 Parser combinators as the core architecture + +Configliere's architecture is built on small, composable parser functions. `field()` parses a single value. `object()` composes fields. `commands()` composes command variants. `program()` adds help/version screening. `inject()` defers a phase behind a dependency. + +Parsers compose by nesting. There is no orchestration layer that "knows about" all the parsers — each parser receives a context, processes its scope, and returns a result. Composition is structural, not procedural. + +### 2.3 Provenance and inspectability + +Every configuration value MUST be traceable to its source. Configliere tracks which source provided each field's value — whether CLI argument, environment variable, config-file entry, default, or none — and makes this information available through the info tree. + +The project's goal is that the origin of every configuration value should be inspectable and never mysterious. + +### 2.4 Type inference as a first-class requirement + +TypeScript type inference for parser composition is a correctness requirement, not a cosmetic concern. When a user composes parsers from literal object maps, the resulting types MUST be inferred without explicit annotation. Inference failures in the library's generic signatures are treated as bugs with blocking priority. + +### 2.5 Structural derivation of help and introspection + +Help text and parser introspection MUST derive from parser structure, not from manually maintained documentation. Each parser contributes to help through its `inspect()` method. There is no separate documentation layer. `format()` renders what `inspect()` reports. + +### 2.6 Compositional phased parsing + +Multi-phase parsing (where later phases depend on earlier results or runtime IO) MUST remain compositional. There is no "preamble hack," callback escape hatch, or side-channel architecture. Each phase is a parser. Phases compose by nesting — a parser's value may contain a factory function that produces the next phase's parser. + +### 2.7 Resistance to special cases + +Configliere is suspicious of special cases. When a behavior appears to require special handling, the preferred response is to find a general mechanism. `program()` is not architecturally special — it is a parser whose dependency type happens to be `void`. `inject()` is not architecturally special — it is a parser whose value happens to be a function. + +--- + +## 3. Terminology + +**Parser.** A value with `parse()`, `inspect()`, and `help()` methods that processes a `ParseContext` and produces a typed result. Parsers are the fundamental building block. + +**Parser composition.** Building larger parsers from smaller ones by nesting. `object()` composes fields. `commands()` composes command variants. Each composition level preserves type inference. + +**Field.** A parser for a single typed value, backed by a Standard Schema validator. A field is the leaf node in a parser tree. It knows how to extract its value from CLI args, env vars, and config-file values. + +**Object parser.** A parser that composes named fields into a typed object. `object({ port: field(type("number")), host: field(type("string")) })` produces `Parser<{ port: number; host: string }>`. + +**Commands parser.** A parser that dispatches to one of several named command parsers based on the first positional CLI argument. Each command has its own inner parser. Commands introduces scoping for config-file values and environment variables. + +**Program.** A parser that screens for `--help` and `--version` flags, sets the root program name, and delegates to a config parser. Program wraps any parser to provide CLI entry-point behavior. + +**Parse result.** The outcome of parsing: `Done` (success with typed value) or `Fail` (failure with error). Both carry a `remainder` of unconsumed input. + +**Remainder.** The unconsumed portion of input after a parser has consumed what it recognizes. Remainder flows upward from child to parent. + +**Source.** A record of where a field's value came from: `{ sourceName, sourceType, value, issues }`. Source types include `"none"`, `"default"`, `"value"` (config file or CLI-injected), and `"env"`. + +**Provenance.** The traceability chain from a field's resolved value back to the source that provided it. Provenance is visible through `FieldInfo.source` and `FieldInfo.sources`. + +**Help.** Human-readable text describing a parser's fields, options, arguments, and commands. Help is derived structurally from `inspect()`, not maintained separately. + +**Inspect.** The canonical parser operation. `inspect(ctx: ParseContext)` walks the parser tree and returns a `ParserInfo` containing the parse result, help metadata, source information, and child info. `parse()` and `help()` derive from `inspect()`. + +**ParseContext.** The immutable, downward-flowing context that carries input data (args, values, envs) and structural scope (progname, path, commands map) through the parser tree. + +**Info tree / ParserInfo.** The tree of `ParserInfo` nodes produced by `inspect()`. The info tree mirrors the parser tree. Each node contains the parse result, remainder, help metadata, and type-specific fields (e.g., `attrs` for objects, `commands` for command sets). + +**Command selection.** The act of choosing which command to execute. Command selection is based on CLI arguments only — the first positional argument (or alias, or default). + +**Command configuration.** The act of providing values to the selected command's fields. Command configuration is tripartite — CLI args, env vars, and config-file values all contribute. + +**Phase.** A parse step in a multi-step pipeline. Phase 1 runs, the application does IO (e.g., loads a config file), then phase 2 runs with enriched input. + +**Dependency-generated phase.** A phase whose parser is constructed at runtime from a dependency value. The parser does not exist until the dependency is resolved. Represented by `inject((dep: D) => Parser)`. + +**Env scoping / command prefix.** The process by which `commands()` transforms the environment-variable map for a matched command, stripping a command-name prefix (e.g., `SERVE_PORT` → `PORT` inside the `serve` command) and narrowing config-file values to the command's subtree. + +--- + +## 4. Core Abstractions + +### 4.1 What a parser represents + +A parser is a composable unit that, given a `ParseContext`, produces a `ParserInfo` containing: + +- a parse result (`Done` or `Fail`), +- a remainder of unconsumed input, +- pre-classified help metadata, +- and type-specific structural information. + +Every parser implements `parse()`, `inspect()`, and `help()`. The current architectural shape of the parser interface is: + +```ts +// Illustrative — reflects the current observed interface shape. +// Exact signatures may evolve; what is normative is the behavioral +// contract described in §4.2, not this specific type definition. +interface Parser = ParserInfo> { + parse(input?: Input, ctx?: ParseContext): ParseResult; + inspect(ctx: ParseContext): Info; + help(input?: Input, ctx?: ParseContext): string; +} +``` + +### 4.2 Parse vs. inspect responsibilities + +The current intended architectural direction is that `inspect()` is the canonical operation. It does the actual work of walking the parser tree, computing results, and building the info tree. + +`parse()` SHOULD be equivalent to `inspect(ctx ?? createContext(input)).result`. + +`help()` SHOULD be equivalent to `format(inspect(ctx ?? createContext(input)))`. + +Implementations SHOULD NOT maintain separate code paths for parsing, help generation, or introspection. The project's clear direction is that all three derive from `inspect()`. + +> **Current intended contract.** The "inspect is the workhorse" principle is supported by direct statements from the project lead and a full observed refactor across all parsers. This specification treats it as the strongest available architectural direction. However, the exact API contract (whether `parse()` literally calls `inspect()` or merely produces equivalent results) is implementation-defined. What the spec requires is behavioral equivalence: `parse()` and `inspect().result` MUST agree for the same input. + +### 4.3 Structural composition expectations + +Parser composition MUST preserve type inference (§13). Container parsers (object, commands, program) MUST embed child parsers and propagate context downward. Each parser MUST build its own `help` contribution in its `inspect()` method — help MUST NOT be reverse-engineered from the info tree by a separate classifier. + +### 4.4 Parser-local vs. orchestration above parsers + +`inspect()` is authoritative for any individual parser. Concerns that span multiple parsers — specifically cross-phase help assembly (§12.4) — sit above `inspect()` and are the application's responsibility. + +--- + +## 5. Input Model + +### 5.1 Supported input sources + +Configliere supports three input sources: + +1. **CLI arguments** (`args: string[]`). Parsed from the command line. Options are `--name value` or `--name=value`. Boolean fields are switches (`--debug` / `--no-debug`). Positional arguments are declared with `cli.argument()`. + +2. **Environment variables** (`envs: { name: string; value: Record }[]`). Each env source is a named record of string key-value pairs. Field paths determine env-key lookup (§10.1). String values are coerced to the field's type. + +3. **Config-file object values** (`values: { name: string; value: unknown }[]`). Each value source is a named object (typically from a parsed JSON/YAML file). Object keys correspond to field names. + +### 5.2 What equivalence means + +Every field SHOULD be configurable through every source, subject to the asymmetries described in §2.1. The same Standard Schema validator MUST validate values regardless of which source provides them. Source priority (§6.3) determines which value wins when multiple sources provide conflicting values. + +### 5.3 What equivalence does not mean + +Source equivalence has explicit exceptions and asymmetries, detailed in §2.1. The most significant: command selection is CLI-only (§9.1), env vars require string coercion, and not every structural configuration concept has a direct env-var equivalent. + +--- + +## 6. Source Model and Provenance + +### 6.1 Source identity + +Every source has a `sourceName` (e.g., `"cli"`, `"env"`, `"config.json"`) and a `sourceType` (one of `"none"`, `"default"`, `"value"`, `"env"`). + +### 6.2 Source tracking + +`FieldInfo` MUST always carry: + +- `source: Source` — the winning source (the one whose value was used), +- `sources: Source[]` — all attempted sources in priority order. + +Source and sources MUST NOT be optional. They MUST be populated for every parsed field, regardless of whether the value came from explicit input or a default. + +### 6.3 Source priority + +This specification provisionally defines the following priority order (highest to lowest): + +1. CLI arguments (injected as `"value"` sources by `scopeInput`) +2. Environment variables (`"env"`) +3. Config-file object values (`"value"`) +4. Default value (`"default"`) +5. None / undefined (`"none"`) + +The winner is the last valid (no validation issues) source in collection order. Since sources are collected in increasing priority order, `findLast` selects the highest-priority valid source. + +> **Current intended contract.** This priority order is implemented in observed code and matches the traditional `cli > env > config > default > none` convention for CLI tools. The project lead has not explicitly stated this ordering as doctrine, but it is consistent with established conventions and with the observed implementation. This specification treats it as provisionally normative — the strongest available reading of the current architecture, likely to be confirmed rather than revised. + +### 6.4 Provenance visibility + +Help output SHOULD display the winning source's provenance (e.g., `[env: PORT=8080]`, `[default: 3000]`, `[config.json: port=8080]`). Invalid sources (those with validation issues) MUST NOT be displayed in help output. + +### 6.5 Known provenance limitation: env-var identity after scoping + +> **Open.** When `scope()` transforms the env map for a command (§10.2), the original env-var key is lost. A user who sets `SERVE_PORT=8080` sees `[env: PORT=8080]` in help output — the connection between what they set and what the system reports is obscured. This is a tension with the project's inspectability emphasis. The spec does not currently require tracking pre-scoping env-var identity, but acknowledges this as a gap in the provenance model. + +--- + +## 7. Precedence Model + +### 7.1 Field-level precedence + +Within a single field, the precedence model is as defined in §6.3: CLI > env > config value > default > none. This MUST be consistent across all parser types. + +### 7.2 Command-scoped precedence + +When a field is inside a command, the field sees a *scoped* environment and value set (§10). Within that scoped set, the same precedence model applies. The scoping itself does not change precedence — it changes which values are visible. + +### 7.3 Scoped vs. global env-var precedence + +> **Transitional.** When both a global env var (e.g., `PORT=3000`) and a command-scoped env var (e.g., `SERVE_PORT=8080`) are present, the scoped value SHOULD take precedence. The current implementation achieves this only when the scoped key appears after the global key in `Object.entries()` iteration order. Implementations SHOULD use a two-pass approach (process non-prefixed keys first, then overwrite with prefix-stripped keys) to guarantee scoped-wins regardless of input ordering. + +--- + +## 8. Parsing Model + +### 8.1 `field(schema, ...mods)` + +**Role:** Parses a single typed value from the available sources. + +**Contract:** +- MUST validate the value against the provided Standard Schema. +- MUST collect all sources (none, default, values, envs) and pick the highest-priority valid one. +- MUST derive its env-var key from `ctx.path` (§10.1). +- MUST report all attempted sources in `FieldInfo.sources`. +- MUST classify itself as an argument (if `argument: true`) or an option in its `help` contribution. + +**Type expectation:** `field(type("number"))` MUST produce `Parser>` without annotation. + +### 8.2 `object(attrs)` + +**Role:** Composes named fields (or partial parser entries) into a typed object. + +**Contract:** +- MUST iterate child entries and call `child.inspect(childCtx)` for each, with `ctx.path` extended by the child's key. +- MUST collect child help items (args, opts, commands) into its own `help`. +- MUST handle metadata-only entries (e.g., `{ description: "..." }`) by defaulting to `field(optionalBoolean)`. +- MUST scope CLI args by matching against child field paths (via `scopeInput`). +- MUST scope config-file values by extracting each child's key from value objects. + +**Type expectation:** `object({ port: field(type("number")), host: field(type("string")) })` MUST produce `Parser<{ port: number; host: string }>` without annotation. + +### 8.3 `commands(map, opts?)` + +**Role:** Dispatches to one of several named command parsers based on CLI argument matching. + +**Contract:** +- MUST match the first positional CLI argument against command names and aliases. +- MUST support a `default` option specifying a fallback command when no name matches. +- MUST scope config-file values and environment variables for each command via `scope()` (§10). +- MUST build `help.commands` listing all available commands. +- MUST NOT select commands based on config-file keys or env-var values (§9.1). + +**Type expectation:** `commands({ run: object({...}), build: object({...}) })` MUST infer a parser whose value type is a union of `Command` with literal string name types. + +### 8.4 `program(opts)` + +**Role:** Provides CLI entry-point behavior — help/version flag screening, root progname, and delegation to a config parser. + +**Contract:** +- MUST screen for `--help`/`-h` and `--version`/`-v` before delegating to the config parser. +- MUST set `progname: [name]` in the root context. +- MUST produce `Parser>` where `T` is the config parser's value type. +- MUST merge preamble options (help, version) into the help output alongside the config parser's help. + +### 8.5 `inject(fn)` + +**Role:** Defers a parse phase behind a dependency. The factory function `fn` produces a parser when called with the dependency value. + +**Contract:** +- In the current architecture, the returned parser's value is the resolve function `(dep: D) => Parser`. [Current API shape — the resolve-function-as-value pattern is the established approach, though the exact value representation is an API design choice that could evolve.] +- MUST pass `ParseContext` through and bake it into the resolved parser via closure. [Core to phased parsing — without this, resolved parsers lose their position in the hierarchy.] +- MUST contribute empty help (no args, opts, or commands) — the resolved parser has its own help. [Directly observed and architecturally required — inject cannot describe what it doesn't yet know.] +- SHOULD NOT call the factory function during `inspect()` unless the dependency type is legitimately satisfiable. When `D` is `void`, calling the factory during `inspect()` is valid (this is how `program()` resolves its config parser). For non-void `D`, calling the factory with a placeholder or invalid argument is not acceptable under the current design direction. + +> **Note.** The project lead stated "probing is disqualifying" but then clarified "just not with invalid arguments." The boundary between valid and invalid probing is not fully settled. The clearest rule: a factory may be called during `inspect()` only with a value of its declared dependency type. + +### 8.6 `constant(value)` + +**Role:** A parser that always succeeds with a fixed value. + +**Contract:** +- MUST return `{ ok: true, value }` regardless of input. +- MUST contribute empty help. + +--- + +## 9. Command Model + +### 9.1 Command selection + +Command selection MUST be based on CLI arguments only. The first positional argument is matched against command names and aliases. If no match is found and a `default` is specified, the default command is selected. If no match and no default, parsing fails with a `NoCommandMatchError`. + +Commands MUST NOT be selected based on config-file keys or environment variables. + +> **Evidence basis.** This is the best-evidenced design decision in the command model. Command selection by config-file key was implemented and then explicitly removed by the project lead. Env-var-based selection was never implemented. This specification treats CLI-only selection as normative based on the strength of that decision, while acknowledging that the project lead has not explicitly stated it as permanent doctrine — only actively chosen it over the alternative. + +### 9.2 Command configuration + +Once a command is selected, its fields MUST be configurable through all three sources (CLI, env, config file). The `scope()` function (§10) narrows the input for each command. + +### 9.3 Default commands + +When `commands(map, { default: "name" })` specifies a default, and no CLI argument matches any command name or alias, the default command MUST be selected. The default command MUST receive scoped input identically to explicitly selected commands. + +### 9.4 Alias behavior + +Command aliases are alternative names for matching. When a command is matched by alias, scoping SHOULD use the canonical command name (the key in the commands map), not the alias. + +> **Note.** This behavior is inferred from the code structure (`match()` returns canonical entries) but has not been directly tested or explicitly stated by the project lead. The spec treats it as the intended direction. + +### 9.5 Canonical naming + +The canonical command name — the key in the commands map — is authoritative for: +- env-var prefix derivation, +- config-file key extraction, +- progname construction, +- help display. + +Aliases are used only for CLI matching. + +### 9.6 Nested commands + +> **Open.** Nested commands (commands within commands) are structurally supported by the parser architecture. The env-scoping mechanism would compose by successive prefix stripping if applied recursively. However, nested command env scoping is not tested, not discussed in the design transcripts, and not confirmed as an intended use case. Implementations MAY support nested commands, but this specification does not guarantee specific nested env-scoping behavior. + +--- + +## 10. Environment-Variable Scoping Model + +### 10.1 Path-based env key derivation + +A field's env-var key MUST be derived from its `ctx.path`: + +1. Each path segment is converted to `UPPER_SNAKE_CASE`. +2. Segments are joined with `_`. + +Example: path `["port"]` → key `PORT`. Path `["serverPort"]` → key `SERVER_PORT`. Path `["server", "port"]` → key `SERVER_PORT`. + +### 10.2 Command prefix stripping + +When `commands()` scopes input for a matched command, the `scope()` function MUST: + +1. **For config-file values:** Extract the subtree keyed by the command name. `{ serve: { port: 8080 } }` → `{ port: 8080 }` for command `serve`. + +2. **For env vars:** Create a new env map where: + - Keys starting with the command prefix (e.g., `SERVE_`) are stripped of the prefix (e.g., `SERVE_PORT` → `PORT`). + - Keys not starting with the prefix are passed through unchanged. + +The command prefix MUST be derived as `toSnake(commandName).toUpperCase() + "_"`. + +> **Implementation note.** The current implementation uses the `ts-case-convert` library for `toSnake` conversion. Whether the spec should define conversion rules independently of this library is an open question (§17). What is normative: the prefix is the UPPER_SNAKE_CASE form of the command name followed by `_`. + +### 10.3 Global env-var pass-through + +> **Transitional.** The current implementation passes non-prefixed env vars through into commands, so a global `DATABASE_URL` is visible inside any command. This behavior is consistent with the project's philosophy but has not been explicitly confirmed as a deliberate design choice. Implementations SHOULD maintain this pass-through behavior. The specification may strengthen this to MUST in a future version. + +### 10.4 Alias names and env keys + +Alias names SHOULD NOT affect env-var prefix derivation. The canonical command name SHOULD be used for the prefix. If `serve` has alias `s`, the env prefix is `SERVE_`, not `S_`. + +> **Note.** This follows from the code structure (canonical name is returned by `match()` and passed to `scope()`), not from an explicit design statement. The spec treats canonical-name scoping as the intended direction. + +### 10.5 Known limitation: prefix collision + +When a field name produces an env key that starts with the command prefix, `scope()` strips the prefix before the field can see it. Example: `serveMode` inside `serve` → env key `SERVE_MODE` → stripped to `MODE` by scoping → field looks for `SERVE_MODE` but finds only `MODE`. + +> **Open.** This collision is a known limitation of the prefix-stripping algorithm. Users SHOULD avoid field names that produce env keys beginning with the command prefix. Whether `scope()` should be made schema-aware to avoid false prefix matches is an open design question. + +--- + +## 11. Help and Introspection Model + +### 11.1 `inspect()` as the architectural center + +The current architectural direction is that `inspect(ctx: ParseContext)` serves as the canonical parser operation. In this model, it: + +1. Accepts a `ParseContext` with input data and structural scope. +2. Walks the parser tree, calling child `inspect()` with derived contexts. +3. Returns a `ParserInfo` containing parse result, help metadata, provenance, child info, and remainder. + +`parse()` and `help()` SHOULD derive from `inspect()` (see §4.2). `parse()` and `inspect().result` MUST agree for the same input. Implementations SHOULD NOT maintain separate code paths for parsing vs. introspection. + +> **Transitional architectural direction.** This section describes the clearest current direction of the architecture, supported by direct statements and observed refactors. The exact boundary between "inspect is the workhorse" as a hard implementation requirement vs. a guiding principle is still being established. What is firmly settled: `parse()` results and `inspect()` results MUST be consistent. What is an architectural direction: that `inspect()` is literally the single entry point from which parse and help derive. + +### 11.2 `ParseContext` + +`ParseContext` carries both input data and structural scope. It MUST flow downward immutably — parents create new contexts for children; contexts MUST NOT be mutated. + +Current fields: + +| Field | Category | Purpose | +|-------|----------|---------| +| `progname: string[]` | Scope | Position in program name hierarchy | +| `path: string[]` | Scope | Position in schema object hierarchy | +| `commands: Record>` | Scope | Sibling commands map | +| `args: string[]` | Input | CLI arguments | +| `values: { name: string; value: unknown }[]` | Input | Config-file values | +| `envs: { name: string; value: Record }[]` | Input | Environment variables | + +`createContext(input?: Input): ParseContext` bridges from user-facing `Input` to internal `ParseContext`, producing empty structural fields. + +> **Transitional.** The six-field structure above describes the current implementation. Whether this exact field set is canonical or subject to evolution is not yet decided. What is settled: ParseContext carries input data and structural scope, and flows downward immutably. What may evolve: the exact set of scope fields, and whether `commands` belongs on the context or on the parser itself. + +### 11.3 `ParserInfo` and the info tree + +The current `ParserInfo` carries the following base fields: + +- `type: string` — discriminator for the info subtype +- `parser: Parser` — back-reference to the producing parser +- `result: ParseResult` — the parse outcome +- `remainder: Input` — unconsumed input +- `help: HelpInfo` — pre-classified help metadata + +These fields MUST be present on every info node. The `result`, `remainder`, and `help` fields are the architectural minimum — they ensure that `parse()` can derive from `inspect().result` and that help can derive from `inspect().help`. + +Specialized info types extend `ParserInfo` with type-specific fields. The current observed set: + +- `FieldInfo`: path, source, sources, required, argument, array, aliases, description, default, boolean +- `ObjectInfo`: `attrs: { [K in keyof T]: ParserInfo }` +- `CommandInfo`: name, description, aliases, config, commands +- `CommandsInfo`: `commands: { [C in T as C["name"]]: CommandInfo }` +- `ProgramInfo`: name, version, `main: ParserInfo` +- `ConstantInfo`: value + +> **Note.** The exact field set on each info subtype reflects the current implementation. The base fields (`result`, `remainder`, `help`) are architecturally required. The `parser` back-reference and `type` discriminator are strongly evidenced. The specialized fields on each subtype are observed and consistent, but their exact set may evolve — for instance, whether `remainder` belongs on `ParserInfo` or on `ParseResult` has been questioned in transcripts. This specification treats the current shape as provisional. + +### 11.4 Help derivation + +Each parser MUST build its `help: HelpInfo` field in its `inspect()`: + +- `field` → classifies itself as arg or opt +- `object` → collects children's help items +- `commands` → lists commands in `help.commands` +- `command` → resets help to its own config's items, sets progname +- `program` → merges preamble opts (help, version) with main help +- `inject` → empty help +- `constant` → empty help + +`format()` reads `info.help` and renders it. Implementations SHOULD NOT use a `classify()` function or `switch (info.type)` dispatcher that reverse-engineers help from info types — each parser is responsible for building its own `help` contribution. + +> **Note.** An earlier `classify()` approach was explicitly rejected by the project lead: "`HelpInfo` is not really a thing. It should be ObjectInfo and it should just reference ParserInfo. Any type discrimination can happen in the actual format()." The per-parser help-building pattern is the clear intended direction. + +### 11.5 Invariants + +Help MUST NOT contain fake path fallbacks (e.g., `|| "field"`, `|| "object"`). + +Progname MUST be consistently propagated. `program()` sets `progname: [name]`. `command()` appends its name. Every container MUST propagate progname to children. + +> **Transitional.** The *mechanism* for progname propagation — whether through `ParseContext.progname`, a mutable `Parser.progname` property, or a `ParserInfo.parent` field — has gone through multiple implementations during the project's evolution. The current observed code uses `ParseContext.progname`. This specification requires the invariant (progname is consistently propagated) without mandating the specific mechanism. + +Path MUST be propagated through `ParseContext`. `object()` appends each child's key. Fields use path for CLI option key derivation and env-var key mapping. + +--- + +## 12. Phased Parsing Model + +### 12.1 Static phased parsing + +When both parsers are known at construction time, all help is available before any phase executes. This is the simplest case and MUST be fully supported. + +### 12.2 Dependency-generated phases + +`inject((dep: D) => Parser)` represents a phase whose parser depends on runtime state. The factory function produces the parser when called with the dependency value. + +The contract for `inject()` is defined in §8.5. The key phased-parsing implications: + +- When `D` is `void`, the factory MAY be called during `inspect()` (this is how `program()` resolves its config parser and provides full help). +- For non-void `D`, the factory SHOULD NOT be called until the caller provides a legitimate dependency value. Calling the factory with a placeholder or invalid argument to obtain a "preview" parser for help purposes is not acceptable under the current design direction (see §8.5 note on probing). +- After the caller resolves the dependency, the returned parser has baked context and full help is available through standard `inspect()`. + +> **Note.** The now-removed `step()` combinator did call `resume(undefined as never)` to create a preview parser for help. This worked for cases where parser *structure* was dependency-independent. Whether a similar mechanism should be reintroduced as an opt-in pattern (e.g., `inject(fn, { preview: fn(defaultDeps) })`) remains an open design question. + +### 12.3 What unified help can and cannot mean + +Full exact pre-resolution help for a parser whose structure depends on an unresolved dependency is **not achievable**. You cannot enumerate commands contributed by plugins that haven't been loaded. + +The spec defines the following help levels: + +| Phase type | Pre-resolution help | Post-resolution help | +|-----------|--------------------|--------------------| +| Static | Complete | Complete | +| Enriched values (static structure) | Complete | Complete | +| Static structure, dynamic defaults | Structural preview possible | Complete | +| Dependency-generated structure | Phase 1 only | Complete | +| Recursive nesting | Phase 1 only per level | Complete after all resolutions | + +Implementations MUST NOT claim pre-resolution help is complete when it is only partial. If a help formatter encounters an inject boundary, it MUST NOT silently skip it and present the result as complete. + +### 12.4 Cross-phase help assembly + +Cross-phase help assembly — combining help from multiple `inspect()` results — sits **above** `inspect()`. It is the application's responsibility, not a per-parser concern. + +After the application resolves a dependency and obtains the phase 2 parser, it calls `inspect()` on that parser. The resolved parser has baked context, so its help includes correct progname and remainder. The application combines phase 1 and phase 2 help. + +> **Open.** Whether the library should provide a help-accumulation utility (e.g., `mergeHelp(info1, info2)`) or leave this entirely to the application is not yet specified. + +### 12.5 Context baking + +When `inject()` captures a `ParseContext` and the caller invokes the resolve function, the returned parser MUST have the original context baked in. Calling `parse()` or `inspect()` on the resolved parser MUST use the baked context (progname, path, args, values, envs) without the caller needing to provide them again. + +> **Note.** Context baking is the mechanism that makes phased parsing ergonomic — it ensures the resolved parser "remembers" where it sits in the program hierarchy. The exact implementation (closure capture vs. explicit context parameter) is implementation-defined. What the spec requires is the behavioral invariant: the resolved parser behaves as if it has the original context. + +--- + +## 13. Type Inference Contract + +### 13.1 Guaranteed inference for literal composition + +The following inference behaviors MUST hold without explicit annotation **when parsers are composed using inline literal object maps** (the canonical use case): + +1. `object({ key: field(schema) })` MUST infer `Parser<{ key: SchemaType }>`. +2. `commands({ name: { ...object({...}) } })` MUST infer a parser whose type parameter is a union of `Command` with literal `Name` strings. +3. `program({ config: parser })` MUST preserve the config parser's type through to `ProgramType`. +4. `field(schema)` MUST infer `Parser>`. +5. Types inferred at inner composition levels (field → object → commands → program) MUST survive to the outer level without collapsing to `unknown`. + +These guarantees apply to the canonical case of literal composition. For computed or dynamically constructed parser maps, see §13.5. + +### 13.2 Extraction helpers + +Configliere SHOULD provide type-level extraction helpers that take a parser type (`typeof parser`) as input and extract useful derived types. The convention that extraction helpers operate on *parser types* (not on extracted value types) is established by the project lead: "Everything with *Type should take as a parameter a parser." + +The current observed set of helpers includes: + +- `ConfigType

` — extracts `T` from `Parser`. +- `CommandsType

` — extracts the command union. +- `CommandType` — extracts a single command member by name, prettified. +- `ProgramType

` — unwraps `Program` to get `T`. + +What is normative: +- The convention that extraction helpers operate on parser types, not on extracted value types. [Directly stated by the project lead.] +- Any provided extraction helper MUST produce correct types for the supported composition patterns. + +What is the current intended direction but may evolve: +- The exact set and naming of helpers listed above is the current observed roster. Future versions may add, rename, or restructure these helpers as the type-level architecture matures. + +> **Open.** Whether this set is complete — and in particular whether a `ConfigFileType` or similar helper for config-file authoring shapes is needed — is an unresolved question (see §13.7 and §17). + +### 13.3 No hidden defaults + +`Parser` MUST NOT default `T` to `unknown` or `any`. A bare `Parser` without a type argument MUST be a compile error. Generic interfaces and functions in the public API MUST NOT default type parameters to `unknown` or `any`. + +### 13.4 Forbidden patterns + +Public API generic signatures MUST NOT use key-remapping patterns (`[C in T as C["name"]]`) that TypeScript cannot reverse-infer from object literals. + +Type extraction helpers that may produce unions MUST NOT apply non-distributed mapped type transformations (e.g., `{ [K in keyof T]: T[K] }` directly on a union, which collapses to `{}`). + +### 13.5 Computed maps and annotation + +When command maps are built dynamically (via `reduce()`, `Object.fromEntries()`, or similar), TypeScript may erase literal key types before `commands()` is called. This is a TypeScript limitation, not a library defect. + +This specification distinguishes two cases: + +- **Fully runtime-derived maps** (keys from IO or runtime values): inference produces `unknown`. Explicit type annotation is REQUIRED. This is out of scope for the inference guarantee (§16). +- **Statically computable but TypeScript-widened maps** (keys derivable from static information, but erased by `Object.keys()` or `reduce()`): inference requires explicit annotation, but the annotation burden may be reducible. The `satisfies` operator is the RECOMMENDED pattern. + +`commands()` MUST infer correctly from whatever type it receives — the limitation is upstream of the library. + +> **Open.** The exact boundary between "inference must work" and "annotation required" for statically-computable-but-widened maps is not precisely defined. Whether the library should invest in reducing annotation burden (via identity helpers, `const` generics, or documented patterns) is a product-design choice, not a settled contract question. See §17. + +### 13.6 Display quality + +TypeScript tooltip display (e.g., showing `CommandsOf<{...}>` instead of the expanded union) is not under Configliere's control. + +> **Non-normative.** Display quality is an ergonomic goal the project cares about but cannot guarantee. Implementations SHOULD pursue display improvements where possible, but display quality regressions are not treated with the same severity as inference correctness regressions. + +### 13.7 Config-file type extraction + +> **Open.** `ConfigType

` extracts the parsed runtime output type. For `object()` parsers, this coincides with the config-file authoring shape. For `commands()` parsers, it does not — the parsed output is a `Command` union, while a config file would contain a record keyed by command name. Whether a separate extraction concept is needed for config-file typing is an open design question. See §17. + +--- + +## 14. Error Model + +### 14.1 Parse failure + +When a field fails validation, `inspect()` MUST produce a `Fail` result with the validation error. `ObjectValidationError` aggregates child failures with their paths. + +### 14.2 Command mismatch + +When no command matches the input, `commands()` MUST produce a `NoCommandMatchError` listing available commands. + +### 14.3 Help on failure + +When `program()` detects `--help`, it MUST produce help text even if the underlying parse would fail. The help/version screening occurs before config parsing. + +### 14.4 Error provenance + +Validation errors SHOULD include the field path and the failing source's identity where possible. + +--- + +## 15. Extension and Evolution Model + +### 15.1 Parser composition as the extension mechanism + +New functionality SHOULD be added through parser composition — new combinator functions that return `Parser` — not through special-case hooks or configuration flags. + +### 15.2 Phased/injected growth + +Multi-phase parsing MUST remain compositional. New phases are added by nesting `inject()` inside existing parsers. There MUST NOT be a global phase registry or orchestration layer. + +### 15.3 Command extension + +New commands are added by extending the map passed to `commands()`. The type system reflects the extension through union widening. + +### 15.4 Schema enrichment + +Configliere delegates field validation to Standard Schema. New schema libraries can be adopted without changes to Configliere, as long as they implement the Standard Schema specification. + +### 15.5 Constraint against special cases + +Implementations SHOULD NOT introduce behaviors that only work for specific parser types (e.g., help features that only work with `program()` but not with standalone `commands()`). When special behavior is needed, it SHOULD be expressed through the type system (e.g., `D = void` making `inject()` resolvable during inspect). + +--- + +## 16. Explicit Non-Goals + +The following are not supported by the current architecture. Each is graded by the strength of evidence for its exclusion. + +1. **Env-var-based command selection.** No `COMMAND=serve` mechanism exists. Commands are selected by CLI arguments only. [Not implemented; never discussed as a possibility.] + +2. **Config-file-based command selection.** This was implemented and explicitly removed by the project lead. [Directly rejected — the strongest evidence for any non-goal in this list.] + +3. **Exact inference for fully runtime-derived maps.** When keys come from runtime values (e.g., `Object.fromEntries(runtimeKeys.map(...))`), TypeScript erases literal types before Configliere's API is called. This is a TypeScript limitation, not a library defect. [Outside the supported model — not a design rejection, but a platform boundary.] + +4. **Exact pre-resolution help for dependency-generated parser structure.** A parser whose commands depend on unresolved runtime state cannot enumerate those commands before resolution. [Architectural impossibility — not a design choice, but a logical consequence of the phased model.] + +5. **Display-quality guarantees for TypeScript tooltips.** TypeScript controls how type aliases are displayed in editor tooltips. Configliere cares about display quality as an ergonomic goal but cannot guarantee it. [Outside the library's control — not a rejection, but a platform limitation.] + +6. **Structure-only introspection without input.** `inspect()` requires a `ParseContext`. There is currently no "describe your structure without parsing" operation. [Not implemented; not requested in transcripts. Whether it *should* be supported is an open question (§17), but it is not currently offered.] + +7. **Automatic help accumulation across phases.** Cross-phase help assembly is currently the application's responsibility. [Not implemented. Whether the library should provide a utility is an open question (§17), but no such utility currently exists.] + +--- + +## 17. Open Questions and Transitional Areas + +The following areas are unresolved and MUST NOT be silently treated as settled: + +1. **Provenance preservation of original env keys after scoping.** After `scope()` transforms env keys, the original key (e.g., `SERVE_PORT`) is lost. This conflicts with the project's inspectability emphasis. Whether to track pre-scoping identity is an open design question. + +2. **Nested command env semantics.** Whether `API_USERS_PORT` should reach a `port` field inside `api > users` is mechanically plausible but untested and undiscussed. The spec does not guarantee specific nested-command env behavior. + +3. **Config-file authoring type extraction.** `ConfigType

` extracts the parsed output type, not the config-file authoring shape. For commands, these differ. Whether a separate `ConfigFileType

` or equivalent is needed is unresolved. + +4. **Computed-map inference boundary.** Where "inference must always work" stops and "annotation required" begins is not precisely defined for statically-computable-but-TypeScript-widened maps. + +5. **Scoped-wins ordering guarantee.** Whether `scope()` must guarantee that command-prefixed env vars always beat global env vars of the same derived key, regardless of insertion order, is not yet normative. + +6. **Prefix collision handling.** Whether `scope()` should be made schema-aware to avoid false prefix matches on field names is unresolved. + +7. **`ParseContext` field set.** Whether the current six-field `ParseContext` structure is canonical or subject to evolution is not yet decided. + +8. **Help accumulation API.** Whether the library provides a utility for combining help from multiple `inspect()` results across phases is not specified. + +9. **Progname propagation mechanism.** The most recent observed code uses `ParseContext.progname`. Whether a mutable `Parser.progname` property or a `ParserInfo.parent` field also participate is an implementation detail that may evolve. + +10. **Command-scoped help dispatch.** Whether `commands.help()` should match against args and delegate to the matched command's help, or always show the top-level listing, is unresolved. + +11. **`toSnake` dependency.** Env-key derivation currently depends on the `ts-case-convert` library for `toSnake` conversion. Whether the spec should define conversion rules independently is open. + +--- + +## 18. Normative Test Agenda + +The following test families MUST be maintained. This section names required test categories, not exhaustive test cases. + +Each test item is marked by purpose: +- **[Verify current]** — confirms behavior observed working in the current implementation. +- **[Lock down contract]** — asserts an intended invariant that this specification treats as normative. + +### 18.1 Source precedence + +- CLI args beat env vars [Lock down contract] +- env vars beat config values [Lock down contract] +- config values beat defaults [Lock down contract] +- defaults beat none [Lock down contract] +- multiple sources provide valid values for the same field [Lock down contract] + +### 18.2 Source provenance + +- `FieldInfo.source` and `FieldInfo.sources` are always populated [Lock down contract] +- source types are correct [Verify current] +- the winning source is identified [Lock down contract] +- invalid sources are not displayed in help output [Verify current] + +### 18.3 Object parsing + +- correct type inference from literal maps [Lock down contract] +- value extraction from all three sources [Lock down contract] +- CLI arg matching via `scopeInput` [Verify current] +- path propagation to children [Lock down contract] +- metadata-only entry defaulting [Verify current] + +### 18.4 Command selection and configuration + +- command matching by name [Lock down contract] +- command matching by alias [Lock down contract] +- default command selection [Lock down contract] +- config-file value scoping by command key [Lock down contract] +- env-var scoping by command prefix [Lock down contract] +- tripartite configuration for selected commands [Lock down contract] +- `NoCommandMatchError` when no command matches [Verify current] + +### 18.5 Environment-variable scoping + +- scoped env var reaches the correct field [Verify current] +- global env var reaches a field inside a command [Lock down contract] +- scoped env wins over global env of the same derived key [Lock down contract] +- alias invocation uses canonical name for scoping [Lock down contract] +- default command receives scoped env vars [Lock down contract] + +### 18.6 Help derivation + +- `format()` reads `info.help` directly [Lock down contract] +- field classification (arg vs. opt) [Verify current] +- object collection of children's help items [Verify current] +- commands listing in help output [Verify current] +- program merging of preamble opts with main help [Verify current] +- progname propagation in help output [Lock down contract] + +### 18.7 `inspect()` invariants + +- `parse()` results and `inspect().result` agree for the same input [Lock down contract] +- `help()` output and `format(inspect())` agree for the same input [Lock down contract] +- every `ParserInfo` has result, remainder, and help [Lock down contract] +- container parsers embed child info [Verify current] +- `FieldInfo` always has source and sources [Lock down contract] + +### 18.8 Phased parsing + +- inject returns empty help [Verify current] +- resolved parser has full help [Lock down contract] +- resolved parser has baked context [Lock down contract] +- context (progname, args) flows through resolution [Lock down contract] + +### 18.9 Type inference + +Compile-time type tests: +- `object()` infers exact value type from literal map [Lock down contract] +- `commands()` infers exact command union with literal name strings [Lock down contract] +- `ConfigType` extracts `T` from `Parser` [Lock down contract] +- `CommandType` extracts named command member [Lock down contract] +- `ProgramType` extracts inner config type [Lock down contract] +- `commands()` does not collapse to `Command` [Lock down contract] +- `ConfigType` on command union does not collapse to `{}` [Lock down contract] +- bare `Parser` without type argument fails to compile [Lock down contract] + +### 18.10 Helper extraction + +Tests MUST verify that provided extraction helpers (currently `ConfigType`, `CommandType`, `CommandsType`, `ProgramType`) extract correct types from parser instances. [Lock down contract — for whatever helpers exist at the time.] If the helper set evolves, tests MUST cover whatever helpers are provided. + +--- + +*This specification is derived from reverse-engineering research across 13 transcript sessions and current library source code. Items marked **Open** or **Transitional** represent areas where the evidence does not support firm normative language. Interface snippets labeled **Illustrative** show the current observed shape rather than an exact permanent contract. Evidence-graded notes throughout distinguish settled contract from current intended direction and provisional interpretation. The specification aims to be strong enough to guide implementation and testing while remaining honest about what is established and what is still being determined.* From 2f0f7e601f44ae259b9f59d1f83eda72a53476b0 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Fri, 3 Apr 2026 10:50:58 -0400 Subject: [PATCH 2/3] Format specs/configliere-spec.md with deno fmt Formatting-only: line wrapping, trailing-space line breaks to backslash continuations, asterisk-italics to underscore-italics. No content changes. --- specs/configliere-spec.md | 863 +++++++++++++++++++++++++++++--------- 1 file changed, 654 insertions(+), 209 deletions(-) diff --git a/specs/configliere-spec.md b/specs/configliere-spec.md index 5f373b0..372ed3f 100644 --- a/specs/configliere-spec.md +++ b/specs/configliere-spec.md @@ -1,17 +1,32 @@ # Configliere Specification -**Version:** 0.4 — Draft (final sentence-level calibration) -**Status:** Normative draft for repository inclusion -**Derived from:** Reverse-engineering research and focused research briefs (phased parsing, type inference, introspection architecture, environment-variable scoping), grounded in 13 transcript sessions and current library source code. -**Revision note:** This version makes a final sentence-level pass. Source equivalence claims are scoped with SHOULD where source-form limitations apply. Source priority is labeled as provisionally normative. The inject resolve-function pattern is labeled as current API shape. Helper extraction obligation distinguishes the convention (normative) from the roster (current). Test agenda items are annotated by purpose: [Verify current] vs. [Lock down contract]. +**Version:** 0.4 — Draft (final sentence-level calibration)\ +**Status:** Normative draft for repository inclusion\ +**Derived from:** Reverse-engineering research and focused research briefs +(phased parsing, type inference, introspection architecture, +environment-variable scoping), grounded in 13 transcript sessions and current +library source code.\ +**Revision note:** This version makes a final sentence-level pass. Source +equivalence claims are scoped with SHOULD where source-form limitations apply. +Source priority is labeled as provisionally normative. The inject +resolve-function pattern is labeled as current API shape. Helper extraction +obligation distinguishes the convention (normative) from the roster (current). +Test agenda items are annotated by purpose: [Verify current] vs. [Lock down +contract]. --- ## 1. Purpose -Configliere is a parser-combinator configuration system for TypeScript applications that run across CLI, server, and tooling contexts. +Configliere is a parser-combinator configuration system for TypeScript +applications that run across CLI, server, and tooling contexts. -Configliere treats application configuration as a **single pre-validated value** rather than a scattered set of ad hoc reads at runtime. A Configliere program defines a schema of typed fields, composes them into objects and commands, and parses input from three unified sources — CLI arguments, environment variables, and config-file object values — into one validated result with full source provenance. +Configliere treats application configuration as a **single pre-validated value** +rather than a scattered set of ad hoc reads at runtime. A Configliere program +defines a schema of typed fields, composes them into objects and commands, and +parses input from three unified sources — CLI arguments, environment variables, +and config-file object values — into one validated result with full source +provenance. The system produces: @@ -20,7 +35,8 @@ The system produces: - structurally derived help text, - and a parser tree that is inspectable at any point. -Configliere uses Standard Schema validators for field-level validation, making it compatible with arktype, zod, valibot, and other conforming schema libraries. +Configliere uses Standard Schema validators for field-level validation, making +it compatible with arktype, zod, valibot, and other conforming schema libraries. --- @@ -28,86 +44,164 @@ Configliere uses Standard Schema validators for field-level validation, making i ### 2.1 Source equivalence -Source equivalence is a core design goal: CLI arguments, environment variables, and config-file object values are treated as conceptually equivalent input channels for *field-level configuration*. A field that accepts `--port 8080` from the CLI SHOULD also accept `PORT=8080` from the environment and `{ port: 8080 }` from a config file. The same schema validates all three. +Source equivalence is a core design goal: CLI arguments, environment variables, +and config-file object values are treated as conceptually equivalent input +channels for _field-level configuration_. A field that accepts `--port 8080` +from the CLI SHOULD also accept `PORT=8080` from the environment and +`{ port: 8080 }` from a config file. The same schema validates all three. -Source equivalence applies to the field/configuration model — the space of typed scalar and simple values that fields can represent. It does not imply perfect symmetry across all configuration concepts and source forms: +Source equivalence applies to the field/configuration model — the space of typed +scalar and simple values that fields can represent. It does not imply perfect +symmetry across all configuration concepts and source forms: -- Command *selection* is CLI-only (§9.1). This is an explicit asymmetry, not a gap. +- Command _selection_ is CLI-only (§9.1). This is an explicit asymmetry, not a + gap. - Env vars undergo string coercion that CLI and config values do not require. -- Env vars have no natural representation for nested objects or arrays beyond simple conventions (e.g., comma-separated lists). -- Not every structural concept (e.g., a command map, a nested object tree) has a direct env-var equivalent. - -Within the supported field model, every field SHOULD be configurable through every source channel, and every source MUST be validated by the same schema. Where a source cannot naturally represent a field's value (e.g., an env var for a deeply nested object), the limitation is inherent to the source form, not a violation of source equivalence. The goal is that a user who knows one source can predict the behavior of another — not that every source is syntactically identical or structurally equivalent. +- Env vars have no natural representation for nested objects or arrays beyond + simple conventions (e.g., comma-separated lists). +- Not every structural concept (e.g., a command map, a nested object tree) has a + direct env-var equivalent. + +Within the supported field model, every field SHOULD be configurable through +every source channel, and every source MUST be validated by the same schema. +Where a source cannot naturally represent a field's value (e.g., an env var for +a deeply nested object), the limitation is inherent to the source form, not a +violation of source equivalence. The goal is that a user who knows one source +can predict the behavior of another — not that every source is syntactically +identical or structurally equivalent. ### 2.2 Parser combinators as the core architecture -Configliere's architecture is built on small, composable parser functions. `field()` parses a single value. `object()` composes fields. `commands()` composes command variants. `program()` adds help/version screening. `inject()` defers a phase behind a dependency. +Configliere's architecture is built on small, composable parser functions. +`field()` parses a single value. `object()` composes fields. `commands()` +composes command variants. `program()` adds help/version screening. `inject()` +defers a phase behind a dependency. -Parsers compose by nesting. There is no orchestration layer that "knows about" all the parsers — each parser receives a context, processes its scope, and returns a result. Composition is structural, not procedural. +Parsers compose by nesting. There is no orchestration layer that "knows about" +all the parsers — each parser receives a context, processes its scope, and +returns a result. Composition is structural, not procedural. ### 2.3 Provenance and inspectability -Every configuration value MUST be traceable to its source. Configliere tracks which source provided each field's value — whether CLI argument, environment variable, config-file entry, default, or none — and makes this information available through the info tree. +Every configuration value MUST be traceable to its source. Configliere tracks +which source provided each field's value — whether CLI argument, environment +variable, config-file entry, default, or none — and makes this information +available through the info tree. -The project's goal is that the origin of every configuration value should be inspectable and never mysterious. +The project's goal is that the origin of every configuration value should be +inspectable and never mysterious. ### 2.4 Type inference as a first-class requirement -TypeScript type inference for parser composition is a correctness requirement, not a cosmetic concern. When a user composes parsers from literal object maps, the resulting types MUST be inferred without explicit annotation. Inference failures in the library's generic signatures are treated as bugs with blocking priority. +TypeScript type inference for parser composition is a correctness requirement, +not a cosmetic concern. When a user composes parsers from literal object maps, +the resulting types MUST be inferred without explicit annotation. Inference +failures in the library's generic signatures are treated as bugs with blocking +priority. ### 2.5 Structural derivation of help and introspection -Help text and parser introspection MUST derive from parser structure, not from manually maintained documentation. Each parser contributes to help through its `inspect()` method. There is no separate documentation layer. `format()` renders what `inspect()` reports. +Help text and parser introspection MUST derive from parser structure, not from +manually maintained documentation. Each parser contributes to help through its +`inspect()` method. There is no separate documentation layer. `format()` renders +what `inspect()` reports. ### 2.6 Compositional phased parsing -Multi-phase parsing (where later phases depend on earlier results or runtime IO) MUST remain compositional. There is no "preamble hack," callback escape hatch, or side-channel architecture. Each phase is a parser. Phases compose by nesting — a parser's value may contain a factory function that produces the next phase's parser. +Multi-phase parsing (where later phases depend on earlier results or runtime IO) +MUST remain compositional. There is no "preamble hack," callback escape hatch, +or side-channel architecture. Each phase is a parser. Phases compose by nesting +— a parser's value may contain a factory function that produces the next phase's +parser. ### 2.7 Resistance to special cases -Configliere is suspicious of special cases. When a behavior appears to require special handling, the preferred response is to find a general mechanism. `program()` is not architecturally special — it is a parser whose dependency type happens to be `void`. `inject()` is not architecturally special — it is a parser whose value happens to be a function. +Configliere is suspicious of special cases. When a behavior appears to require +special handling, the preferred response is to find a general mechanism. +`program()` is not architecturally special — it is a parser whose dependency +type happens to be `void`. `inject()` is not architecturally special — it is a +parser whose value happens to be a function. --- ## 3. Terminology -**Parser.** A value with `parse()`, `inspect()`, and `help()` methods that processes a `ParseContext` and produces a typed result. Parsers are the fundamental building block. +**Parser.** A value with `parse()`, `inspect()`, and `help()` methods that +processes a `ParseContext` and produces a typed result. Parsers are the +fundamental building block. -**Parser composition.** Building larger parsers from smaller ones by nesting. `object()` composes fields. `commands()` composes command variants. Each composition level preserves type inference. +**Parser composition.** Building larger parsers from smaller ones by nesting. +`object()` composes fields. `commands()` composes command variants. Each +composition level preserves type inference. -**Field.** A parser for a single typed value, backed by a Standard Schema validator. A field is the leaf node in a parser tree. It knows how to extract its value from CLI args, env vars, and config-file values. +**Field.** A parser for a single typed value, backed by a Standard Schema +validator. A field is the leaf node in a parser tree. It knows how to extract +its value from CLI args, env vars, and config-file values. -**Object parser.** A parser that composes named fields into a typed object. `object({ port: field(type("number")), host: field(type("string")) })` produces `Parser<{ port: number; host: string }>`. +**Object parser.** A parser that composes named fields into a typed object. +`object({ port: field(type("number")), host: field(type("string")) })` produces +`Parser<{ port: number; host: string }>`. -**Commands parser.** A parser that dispatches to one of several named command parsers based on the first positional CLI argument. Each command has its own inner parser. Commands introduces scoping for config-file values and environment variables. +**Commands parser.** A parser that dispatches to one of several named command +parsers based on the first positional CLI argument. Each command has its own +inner parser. Commands introduces scoping for config-file values and environment +variables. -**Program.** A parser that screens for `--help` and `--version` flags, sets the root program name, and delegates to a config parser. Program wraps any parser to provide CLI entry-point behavior. +**Program.** A parser that screens for `--help` and `--version` flags, sets the +root program name, and delegates to a config parser. Program wraps any parser to +provide CLI entry-point behavior. -**Parse result.** The outcome of parsing: `Done` (success with typed value) or `Fail` (failure with error). Both carry a `remainder` of unconsumed input. +**Parse result.** The outcome of parsing: `Done` (success with typed value) +or `Fail` (failure with error). Both carry a `remainder` of unconsumed input. -**Remainder.** The unconsumed portion of input after a parser has consumed what it recognizes. Remainder flows upward from child to parent. +**Remainder.** The unconsumed portion of input after a parser has consumed what +it recognizes. Remainder flows upward from child to parent. -**Source.** A record of where a field's value came from: `{ sourceName, sourceType, value, issues }`. Source types include `"none"`, `"default"`, `"value"` (config file or CLI-injected), and `"env"`. +**Source.** A record of where a field's value came from: +`{ sourceName, sourceType, value, issues }`. Source types include `"none"`, +`"default"`, `"value"` (config file or CLI-injected), and `"env"`. -**Provenance.** The traceability chain from a field's resolved value back to the source that provided it. Provenance is visible through `FieldInfo.source` and `FieldInfo.sources`. +**Provenance.** The traceability chain from a field's resolved value back to the +source that provided it. Provenance is visible through `FieldInfo.source` and +`FieldInfo.sources`. -**Help.** Human-readable text describing a parser's fields, options, arguments, and commands. Help is derived structurally from `inspect()`, not maintained separately. +**Help.** Human-readable text describing a parser's fields, options, arguments, +and commands. Help is derived structurally from `inspect()`, not maintained +separately. -**Inspect.** The canonical parser operation. `inspect(ctx: ParseContext)` walks the parser tree and returns a `ParserInfo` containing the parse result, help metadata, source information, and child info. `parse()` and `help()` derive from `inspect()`. +**Inspect.** The canonical parser operation. `inspect(ctx: ParseContext)` walks +the parser tree and returns a `ParserInfo` containing the parse result, help +metadata, source information, and child info. `parse()` and `help()` derive from +`inspect()`. -**ParseContext.** The immutable, downward-flowing context that carries input data (args, values, envs) and structural scope (progname, path, commands map) through the parser tree. +**ParseContext.** The immutable, downward-flowing context that carries input +data (args, values, envs) and structural scope (progname, path, commands map) +through the parser tree. -**Info tree / ParserInfo.** The tree of `ParserInfo` nodes produced by `inspect()`. The info tree mirrors the parser tree. Each node contains the parse result, remainder, help metadata, and type-specific fields (e.g., `attrs` for objects, `commands` for command sets). +**Info tree / ParserInfo.** The tree of `ParserInfo` nodes produced by +`inspect()`. The info tree mirrors the parser tree. Each node contains the parse +result, remainder, help metadata, and type-specific fields (e.g., `attrs` for +objects, `commands` for command sets). -**Command selection.** The act of choosing which command to execute. Command selection is based on CLI arguments only — the first positional argument (or alias, or default). +**Command selection.** The act of choosing which command to execute. Command +selection is based on CLI arguments only — the first positional argument (or +alias, or default). -**Command configuration.** The act of providing values to the selected command's fields. Command configuration is tripartite — CLI args, env vars, and config-file values all contribute. +**Command configuration.** The act of providing values to the selected command's +fields. Command configuration is tripartite — CLI args, env vars, and +config-file values all contribute. -**Phase.** A parse step in a multi-step pipeline. Phase 1 runs, the application does IO (e.g., loads a config file), then phase 2 runs with enriched input. +**Phase.** A parse step in a multi-step pipeline. Phase 1 runs, the application +does IO (e.g., loads a config file), then phase 2 runs with enriched input. -**Dependency-generated phase.** A phase whose parser is constructed at runtime from a dependency value. The parser does not exist until the dependency is resolved. Represented by `inject((dep: D) => Parser)`. +**Dependency-generated phase.** A phase whose parser is constructed at runtime +from a dependency value. The parser does not exist until the dependency is +resolved. Represented by `inject((dep: D) => Parser)`. -**Env scoping / command prefix.** The process by which `commands()` transforms the environment-variable map for a matched command, stripping a command-name prefix (e.g., `SERVE_PORT` → `PORT` inside the `serve` command) and narrowing config-file values to the command's subtree. +**Env scoping / command prefix.** The process by which `commands()` transforms +the environment-variable map for a matched command, stripping a command-name +prefix (e.g., `SERVE_PORT` → `PORT` inside the `serve` command) and narrowing +config-file values to the command's subtree. --- @@ -115,14 +209,16 @@ Configliere is suspicious of special cases. When a behavior appears to require s ### 4.1 What a parser represents -A parser is a composable unit that, given a `ParseContext`, produces a `ParserInfo` containing: +A parser is a composable unit that, given a `ParseContext`, produces a +`ParserInfo` containing: - a parse result (`Done` or `Fail`), - a remainder of unconsumed input, - pre-classified help metadata, - and type-specific structural information. -Every parser implements `parse()`, `inspect()`, and `help()`. The current architectural shape of the parser interface is: +Every parser implements `parse()`, `inspect()`, and `help()`. The current +architectural shape of the parser interface is: ```ts // Illustrative — reflects the current observed interface shape. @@ -137,23 +233,39 @@ interface Parser = ParserInfo> { ### 4.2 Parse vs. inspect responsibilities -The current intended architectural direction is that `inspect()` is the canonical operation. It does the actual work of walking the parser tree, computing results, and building the info tree. +The current intended architectural direction is that `inspect()` is the +canonical operation. It does the actual work of walking the parser tree, +computing results, and building the info tree. `parse()` SHOULD be equivalent to `inspect(ctx ?? createContext(input)).result`. `help()` SHOULD be equivalent to `format(inspect(ctx ?? createContext(input)))`. -Implementations SHOULD NOT maintain separate code paths for parsing, help generation, or introspection. The project's clear direction is that all three derive from `inspect()`. +Implementations SHOULD NOT maintain separate code paths for parsing, help +generation, or introspection. The project's clear direction is that all three +derive from `inspect()`. -> **Current intended contract.** The "inspect is the workhorse" principle is supported by direct statements from the project lead and a full observed refactor across all parsers. This specification treats it as the strongest available architectural direction. However, the exact API contract (whether `parse()` literally calls `inspect()` or merely produces equivalent results) is implementation-defined. What the spec requires is behavioral equivalence: `parse()` and `inspect().result` MUST agree for the same input. +> **Current intended contract.** The "inspect is the workhorse" principle is +> supported by direct statements from the project lead and a full observed +> refactor across all parsers. This specification treats it as the strongest +> available architectural direction. However, the exact API contract (whether +> `parse()` literally calls `inspect()` or merely produces equivalent results) +> is implementation-defined. What the spec requires is behavioral equivalence: +> `parse()` and `inspect().result` MUST agree for the same input. ### 4.3 Structural composition expectations -Parser composition MUST preserve type inference (§13). Container parsers (object, commands, program) MUST embed child parsers and propagate context downward. Each parser MUST build its own `help` contribution in its `inspect()` method — help MUST NOT be reverse-engineered from the info tree by a separate classifier. +Parser composition MUST preserve type inference (§13). Container parsers +(object, commands, program) MUST embed child parsers and propagate context +downward. Each parser MUST build its own `help` contribution in its `inspect()` +method — help MUST NOT be reverse-engineered from the info tree by a separate +classifier. ### 4.4 Parser-local vs. orchestration above parsers -`inspect()` is authoritative for any individual parser. Concerns that span multiple parsers — specifically cross-phase help assembly (§12.4) — sit above `inspect()` and are the application's responsibility. +`inspect()` is authoritative for any individual parser. Concerns that span +multiple parsers — specifically cross-phase help assembly (§12.4) — sit above +`inspect()` and are the application's responsibility. --- @@ -163,19 +275,32 @@ Parser composition MUST preserve type inference (§13). Container parsers (objec Configliere supports three input sources: -1. **CLI arguments** (`args: string[]`). Parsed from the command line. Options are `--name value` or `--name=value`. Boolean fields are switches (`--debug` / `--no-debug`). Positional arguments are declared with `cli.argument()`. +1. **CLI arguments** (`args: string[]`). Parsed from the command line. Options + are `--name value` or `--name=value`. Boolean fields are switches (`--debug` + / `--no-debug`). Positional arguments are declared with `cli.argument()`. -2. **Environment variables** (`envs: { name: string; value: Record }[]`). Each env source is a named record of string key-value pairs. Field paths determine env-key lookup (§10.1). String values are coerced to the field's type. +2. **Environment variables** + (`envs: { name: string; value: Record }[]`). Each env source + is a named record of string key-value pairs. Field paths determine env-key + lookup (§10.1). String values are coerced to the field's type. -3. **Config-file object values** (`values: { name: string; value: unknown }[]`). Each value source is a named object (typically from a parsed JSON/YAML file). Object keys correspond to field names. +3. **Config-file object values** (`values: { name: string; value: unknown }[]`). + Each value source is a named object (typically from a parsed JSON/YAML file). + Object keys correspond to field names. ### 5.2 What equivalence means -Every field SHOULD be configurable through every source, subject to the asymmetries described in §2.1. The same Standard Schema validator MUST validate values regardless of which source provides them. Source priority (§6.3) determines which value wins when multiple sources provide conflicting values. +Every field SHOULD be configurable through every source, subject to the +asymmetries described in §2.1. The same Standard Schema validator MUST validate +values regardless of which source provides them. Source priority (§6.3) +determines which value wins when multiple sources provide conflicting values. ### 5.3 What equivalence does not mean -Source equivalence has explicit exceptions and asymmetries, detailed in §2.1. The most significant: command selection is CLI-only (§9.1), env vars require string coercion, and not every structural configuration concept has a direct env-var equivalent. +Source equivalence has explicit exceptions and asymmetries, detailed in §2.1. +The most significant: command selection is CLI-only (§9.1), env vars require +string coercion, and not every structural configuration concept has a direct +env-var equivalent. --- @@ -183,7 +308,8 @@ Source equivalence has explicit exceptions and asymmetries, detailed in §2.1. T ### 6.1 Source identity -Every source has a `sourceName` (e.g., `"cli"`, `"env"`, `"config.json"`) and a `sourceType` (one of `"none"`, `"default"`, `"value"`, `"env"`). +Every source has a `sourceName` (e.g., `"cli"`, `"env"`, `"config.json"`) and a +`sourceType` (one of `"none"`, `"default"`, `"value"`, `"env"`). ### 6.2 Source tracking @@ -192,11 +318,13 @@ Every source has a `sourceName` (e.g., `"cli"`, `"env"`, `"config.json"`) and a - `source: Source` — the winning source (the one whose value was used), - `sources: Source[]` — all attempted sources in priority order. -Source and sources MUST NOT be optional. They MUST be populated for every parsed field, regardless of whether the value came from explicit input or a default. +Source and sources MUST NOT be optional. They MUST be populated for every parsed +field, regardless of whether the value came from explicit input or a default. ### 6.3 Source priority -This specification provisionally defines the following priority order (highest to lowest): +This specification provisionally defines the following priority order (highest +to lowest): 1. CLI arguments (injected as `"value"` sources by `scopeInput`) 2. Environment variables (`"env"`) @@ -204,17 +332,33 @@ This specification provisionally defines the following priority order (highest t 4. Default value (`"default"`) 5. None / undefined (`"none"`) -The winner is the last valid (no validation issues) source in collection order. Since sources are collected in increasing priority order, `findLast` selects the highest-priority valid source. +The winner is the last valid (no validation issues) source in collection order. +Since sources are collected in increasing priority order, `findLast` selects the +highest-priority valid source. -> **Current intended contract.** This priority order is implemented in observed code and matches the traditional `cli > env > config > default > none` convention for CLI tools. The project lead has not explicitly stated this ordering as doctrine, but it is consistent with established conventions and with the observed implementation. This specification treats it as provisionally normative — the strongest available reading of the current architecture, likely to be confirmed rather than revised. +> **Current intended contract.** This priority order is implemented in observed +> code and matches the traditional `cli > env > config > default > none` +> convention for CLI tools. The project lead has not explicitly stated this +> ordering as doctrine, but it is consistent with established conventions and +> with the observed implementation. This specification treats it as +> provisionally normative — the strongest available reading of the current +> architecture, likely to be confirmed rather than revised. ### 6.4 Provenance visibility -Help output SHOULD display the winning source's provenance (e.g., `[env: PORT=8080]`, `[default: 3000]`, `[config.json: port=8080]`). Invalid sources (those with validation issues) MUST NOT be displayed in help output. +Help output SHOULD display the winning source's provenance (e.g., +`[env: PORT=8080]`, `[default: 3000]`, `[config.json: port=8080]`). Invalid +sources (those with validation issues) MUST NOT be displayed in help output. ### 6.5 Known provenance limitation: env-var identity after scoping -> **Open.** When `scope()` transforms the env map for a command (§10.2), the original env-var key is lost. A user who sets `SERVE_PORT=8080` sees `[env: PORT=8080]` in help output — the connection between what they set and what the system reports is obscured. This is a tension with the project's inspectability emphasis. The spec does not currently require tracking pre-scoping env-var identity, but acknowledges this as a gap in the provenance model. +> **Open.** When `scope()` transforms the env map for a command (§10.2), the +> original env-var key is lost. A user who sets `SERVE_PORT=8080` sees +> `[env: PORT=8080]` in help output — the connection between what they set and +> what the system reports is obscured. This is a tension with the project's +> inspectability emphasis. The spec does not currently require tracking +> pre-scoping env-var identity, but acknowledges this as a gap in the provenance +> model. --- @@ -222,15 +366,24 @@ Help output SHOULD display the winning source's provenance (e.g., `[env: PORT=80 ### 7.1 Field-level precedence -Within a single field, the precedence model is as defined in §6.3: CLI > env > config value > default > none. This MUST be consistent across all parser types. +Within a single field, the precedence model is as defined in §6.3: CLI > env > +config value > default > none. This MUST be consistent across all parser types. ### 7.2 Command-scoped precedence -When a field is inside a command, the field sees a *scoped* environment and value set (§10). Within that scoped set, the same precedence model applies. The scoping itself does not change precedence — it changes which values are visible. +When a field is inside a command, the field sees a _scoped_ environment and +value set (§10). Within that scoped set, the same precedence model applies. The +scoping itself does not change precedence — it changes which values are visible. ### 7.3 Scoped vs. global env-var precedence -> **Transitional.** When both a global env var (e.g., `PORT=3000`) and a command-scoped env var (e.g., `SERVE_PORT=8080`) are present, the scoped value SHOULD take precedence. The current implementation achieves this only when the scoped key appears after the global key in `Object.entries()` iteration order. Implementations SHOULD use a two-pass approach (process non-prefixed keys first, then overwrite with prefix-stripped keys) to guarantee scoped-wins regardless of input ordering. +> **Transitional.** When both a global env var (e.g., `PORT=3000`) and a +> command-scoped env var (e.g., `SERVE_PORT=8080`) are present, the scoped value +> SHOULD take precedence. The current implementation achieves this only when the +> scoped key appears after the global key in `Object.entries()` iteration order. +> Implementations SHOULD use a two-pass approach (process non-prefixed keys +> first, then overwrite with prefix-stripped keys) to guarantee scoped-wins +> regardless of input ordering. --- @@ -241,67 +394,105 @@ When a field is inside a command, the field sees a *scoped* environment and valu **Role:** Parses a single typed value from the available sources. **Contract:** + - MUST validate the value against the provided Standard Schema. -- MUST collect all sources (none, default, values, envs) and pick the highest-priority valid one. +- MUST collect all sources (none, default, values, envs) and pick the + highest-priority valid one. - MUST derive its env-var key from `ctx.path` (§10.1). - MUST report all attempted sources in `FieldInfo.sources`. -- MUST classify itself as an argument (if `argument: true`) or an option in its `help` contribution. +- MUST classify itself as an argument (if `argument: true`) or an option in its + `help` contribution. -**Type expectation:** `field(type("number"))` MUST produce `Parser>` without annotation. +**Type expectation:** `field(type("number"))` MUST produce +`Parser>` without annotation. ### 8.2 `object(attrs)` **Role:** Composes named fields (or partial parser entries) into a typed object. **Contract:** -- MUST iterate child entries and call `child.inspect(childCtx)` for each, with `ctx.path` extended by the child's key. + +- MUST iterate child entries and call `child.inspect(childCtx)` for each, with + `ctx.path` extended by the child's key. - MUST collect child help items (args, opts, commands) into its own `help`. -- MUST handle metadata-only entries (e.g., `{ description: "..." }`) by defaulting to `field(optionalBoolean)`. +- MUST handle metadata-only entries (e.g., `{ description: "..." }`) by + defaulting to `field(optionalBoolean)`. - MUST scope CLI args by matching against child field paths (via `scopeInput`). -- MUST scope config-file values by extracting each child's key from value objects. +- MUST scope config-file values by extracting each child's key from value + objects. -**Type expectation:** `object({ port: field(type("number")), host: field(type("string")) })` MUST produce `Parser<{ port: number; host: string }>` without annotation. +**Type expectation:** +`object({ port: field(type("number")), host: field(type("string")) })` MUST +produce `Parser<{ port: number; host: string }>` without annotation. ### 8.3 `commands(map, opts?)` -**Role:** Dispatches to one of several named command parsers based on CLI argument matching. +**Role:** Dispatches to one of several named command parsers based on CLI +argument matching. **Contract:** -- MUST match the first positional CLI argument against command names and aliases. -- MUST support a `default` option specifying a fallback command when no name matches. -- MUST scope config-file values and environment variables for each command via `scope()` (§10). + +- MUST match the first positional CLI argument against command names and + aliases. +- MUST support a `default` option specifying a fallback command when no name + matches. +- MUST scope config-file values and environment variables for each command via + `scope()` (§10). - MUST build `help.commands` listing all available commands. - MUST NOT select commands based on config-file keys or env-var values (§9.1). -**Type expectation:** `commands({ run: object({...}), build: object({...}) })` MUST infer a parser whose value type is a union of `Command` with literal string name types. +**Type expectation:** `commands({ run: object({...}), build: object({...}) })` +MUST infer a parser whose value type is a union of `Command` with +literal string name types. ### 8.4 `program(opts)` -**Role:** Provides CLI entry-point behavior — help/version flag screening, root progname, and delegation to a config parser. +**Role:** Provides CLI entry-point behavior — help/version flag screening, root +progname, and delegation to a config parser. **Contract:** -- MUST screen for `--help`/`-h` and `--version`/`-v` before delegating to the config parser. + +- MUST screen for `--help`/`-h` and `--version`/`-v` before delegating to the + config parser. - MUST set `progname: [name]` in the root context. - MUST produce `Parser>` where `T` is the config parser's value type. -- MUST merge preamble options (help, version) into the help output alongside the config parser's help. +- MUST merge preamble options (help, version) into the help output alongside the + config parser's help. ### 8.5 `inject(fn)` -**Role:** Defers a parse phase behind a dependency. The factory function `fn` produces a parser when called with the dependency value. +**Role:** Defers a parse phase behind a dependency. The factory function `fn` +produces a parser when called with the dependency value. **Contract:** -- In the current architecture, the returned parser's value is the resolve function `(dep: D) => Parser`. [Current API shape — the resolve-function-as-value pattern is the established approach, though the exact value representation is an API design choice that could evolve.] -- MUST pass `ParseContext` through and bake it into the resolved parser via closure. [Core to phased parsing — without this, resolved parsers lose their position in the hierarchy.] -- MUST contribute empty help (no args, opts, or commands) — the resolved parser has its own help. [Directly observed and architecturally required — inject cannot describe what it doesn't yet know.] -- SHOULD NOT call the factory function during `inspect()` unless the dependency type is legitimately satisfiable. When `D` is `void`, calling the factory during `inspect()` is valid (this is how `program()` resolves its config parser). For non-void `D`, calling the factory with a placeholder or invalid argument is not acceptable under the current design direction. -> **Note.** The project lead stated "probing is disqualifying" but then clarified "just not with invalid arguments." The boundary between valid and invalid probing is not fully settled. The clearest rule: a factory may be called during `inspect()` only with a value of its declared dependency type. +- In the current architecture, the returned parser's value is the resolve + function `(dep: D) => Parser`. [Current API shape — the + resolve-function-as-value pattern is the established approach, though the + exact value representation is an API design choice that could evolve.] +- MUST pass `ParseContext` through and bake it into the resolved parser via + closure. [Core to phased parsing — without this, resolved parsers lose their + position in the hierarchy.] +- MUST contribute empty help (no args, opts, or commands) — the resolved parser + has its own help. [Directly observed and architecturally required — inject + cannot describe what it doesn't yet know.] +- SHOULD NOT call the factory function during `inspect()` unless the dependency + type is legitimately satisfiable. When `D` is `void`, calling the factory + during `inspect()` is valid (this is how `program()` resolves its config + parser). For non-void `D`, calling the factory with a placeholder or invalid + argument is not acceptable under the current design direction. + +> **Note.** The project lead stated "probing is disqualifying" but then +> clarified "just not with invalid arguments." The boundary between valid and +> invalid probing is not fully settled. The clearest rule: a factory may be +> called during `inspect()` only with a value of its declared dependency type. ### 8.6 `constant(value)` **Role:** A parser that always succeeds with a fixed value. **Contract:** + - MUST return `{ ok: true, value }` regardless of input. - MUST contribute empty help. @@ -311,29 +502,49 @@ When a field is inside a command, the field sees a *scoped* environment and valu ### 9.1 Command selection -Command selection MUST be based on CLI arguments only. The first positional argument is matched against command names and aliases. If no match is found and a `default` is specified, the default command is selected. If no match and no default, parsing fails with a `NoCommandMatchError`. +Command selection MUST be based on CLI arguments only. The first positional +argument is matched against command names and aliases. If no match is found and +a `default` is specified, the default command is selected. If no match and no +default, parsing fails with a `NoCommandMatchError`. -Commands MUST NOT be selected based on config-file keys or environment variables. +Commands MUST NOT be selected based on config-file keys or environment +variables. -> **Evidence basis.** This is the best-evidenced design decision in the command model. Command selection by config-file key was implemented and then explicitly removed by the project lead. Env-var-based selection was never implemented. This specification treats CLI-only selection as normative based on the strength of that decision, while acknowledging that the project lead has not explicitly stated it as permanent doctrine — only actively chosen it over the alternative. +> **Evidence basis.** This is the best-evidenced design decision in the command +> model. Command selection by config-file key was implemented and then +> explicitly removed by the project lead. Env-var-based selection was never +> implemented. This specification treats CLI-only selection as normative based +> on the strength of that decision, while acknowledging that the project lead +> has not explicitly stated it as permanent doctrine — only actively chosen it +> over the alternative. ### 9.2 Command configuration -Once a command is selected, its fields MUST be configurable through all three sources (CLI, env, config file). The `scope()` function (§10) narrows the input for each command. +Once a command is selected, its fields MUST be configurable through all three +sources (CLI, env, config file). The `scope()` function (§10) narrows the input +for each command. ### 9.3 Default commands -When `commands(map, { default: "name" })` specifies a default, and no CLI argument matches any command name or alias, the default command MUST be selected. The default command MUST receive scoped input identically to explicitly selected commands. +When `commands(map, { default: "name" })` specifies a default, and no CLI +argument matches any command name or alias, the default command MUST be +selected. The default command MUST receive scoped input identically to +explicitly selected commands. ### 9.4 Alias behavior -Command aliases are alternative names for matching. When a command is matched by alias, scoping SHOULD use the canonical command name (the key in the commands map), not the alias. +Command aliases are alternative names for matching. When a command is matched by +alias, scoping SHOULD use the canonical command name (the key in the commands +map), not the alias. -> **Note.** This behavior is inferred from the code structure (`match()` returns canonical entries) but has not been directly tested or explicitly stated by the project lead. The spec treats it as the intended direction. +> **Note.** This behavior is inferred from the code structure (`match()` returns +> canonical entries) but has not been directly tested or explicitly stated by +> the project lead. The spec treats it as the intended direction. ### 9.5 Canonical naming The canonical command name — the key in the commands map — is authoritative for: + - env-var prefix derivation, - config-file key extraction, - progname construction, @@ -343,7 +554,13 @@ Aliases are used only for CLI matching. ### 9.6 Nested commands -> **Open.** Nested commands (commands within commands) are structurally supported by the parser architecture. The env-scoping mechanism would compose by successive prefix stripping if applied recursively. However, nested command env scoping is not tested, not discussed in the design transcripts, and not confirmed as an intended use case. Implementations MAY support nested commands, but this specification does not guarantee specific nested env-scoping behavior. +> **Open.** Nested commands (commands within commands) are structurally +> supported by the parser architecture. The env-scoping mechanism would compose +> by successive prefix stripping if applied recursively. However, nested command +> env scoping is not tested, not discussed in the design transcripts, and not +> confirmed as an intended use case. Implementations MAY support nested +> commands, but this specification does not guarantee specific nested +> env-scoping behavior. --- @@ -356,37 +573,61 @@ A field's env-var key MUST be derived from its `ctx.path`: 1. Each path segment is converted to `UPPER_SNAKE_CASE`. 2. Segments are joined with `_`. -Example: path `["port"]` → key `PORT`. Path `["serverPort"]` → key `SERVER_PORT`. Path `["server", "port"]` → key `SERVER_PORT`. +Example: path `["port"]` → key `PORT`. Path `["serverPort"]` → key +`SERVER_PORT`. Path `["server", "port"]` → key `SERVER_PORT`. ### 10.2 Command prefix stripping -When `commands()` scopes input for a matched command, the `scope()` function MUST: +When `commands()` scopes input for a matched command, the `scope()` function +MUST: -1. **For config-file values:** Extract the subtree keyed by the command name. `{ serve: { port: 8080 } }` → `{ port: 8080 }` for command `serve`. +1. **For config-file values:** Extract the subtree keyed by the command name. + `{ serve: { port: 8080 } }` → `{ port: 8080 }` for command `serve`. 2. **For env vars:** Create a new env map where: - - Keys starting with the command prefix (e.g., `SERVE_`) are stripped of the prefix (e.g., `SERVE_PORT` → `PORT`). + - Keys starting with the command prefix (e.g., `SERVE_`) are stripped of the + prefix (e.g., `SERVE_PORT` → `PORT`). - Keys not starting with the prefix are passed through unchanged. -The command prefix MUST be derived as `toSnake(commandName).toUpperCase() + "_"`. +The command prefix MUST be derived as +`toSnake(commandName).toUpperCase() + "_"`. -> **Implementation note.** The current implementation uses the `ts-case-convert` library for `toSnake` conversion. Whether the spec should define conversion rules independently of this library is an open question (§17). What is normative: the prefix is the UPPER_SNAKE_CASE form of the command name followed by `_`. +> **Implementation note.** The current implementation uses the `ts-case-convert` +> library for `toSnake` conversion. Whether the spec should define conversion +> rules independently of this library is an open question (§17). What is +> normative: the prefix is the UPPER_SNAKE_CASE form of the command name +> followed by `_`. ### 10.3 Global env-var pass-through -> **Transitional.** The current implementation passes non-prefixed env vars through into commands, so a global `DATABASE_URL` is visible inside any command. This behavior is consistent with the project's philosophy but has not been explicitly confirmed as a deliberate design choice. Implementations SHOULD maintain this pass-through behavior. The specification may strengthen this to MUST in a future version. +> **Transitional.** The current implementation passes non-prefixed env vars +> through into commands, so a global `DATABASE_URL` is visible inside any +> command. This behavior is consistent with the project's philosophy but has not +> been explicitly confirmed as a deliberate design choice. Implementations +> SHOULD maintain this pass-through behavior. The specification may strengthen +> this to MUST in a future version. ### 10.4 Alias names and env keys -Alias names SHOULD NOT affect env-var prefix derivation. The canonical command name SHOULD be used for the prefix. If `serve` has alias `s`, the env prefix is `SERVE_`, not `S_`. +Alias names SHOULD NOT affect env-var prefix derivation. The canonical command +name SHOULD be used for the prefix. If `serve` has alias `s`, the env prefix is +`SERVE_`, not `S_`. -> **Note.** This follows from the code structure (canonical name is returned by `match()` and passed to `scope()`), not from an explicit design statement. The spec treats canonical-name scoping as the intended direction. +> **Note.** This follows from the code structure (canonical name is returned by +> `match()` and passed to `scope()`), not from an explicit design statement. The +> spec treats canonical-name scoping as the intended direction. ### 10.5 Known limitation: prefix collision -When a field name produces an env key that starts with the command prefix, `scope()` strips the prefix before the field can see it. Example: `serveMode` inside `serve` → env key `SERVE_MODE` → stripped to `MODE` by scoping → field looks for `SERVE_MODE` but finds only `MODE`. +When a field name produces an env key that starts with the command prefix, +`scope()` strips the prefix before the field can see it. Example: `serveMode` +inside `serve` → env key `SERVE_MODE` → stripped to `MODE` by scoping → field +looks for `SERVE_MODE` but finds only `MODE`. -> **Open.** This collision is a known limitation of the prefix-stripping algorithm. Users SHOULD avoid field names that produce env keys beginning with the command prefix. Whether `scope()` should be made schema-aware to avoid false prefix matches is an open design question. +> **Open.** This collision is a known limitation of the prefix-stripping +> algorithm. Users SHOULD avoid field names that produce env keys beginning with +> the command prefix. Whether `scope()` should be made schema-aware to avoid +> false prefix matches is an open design question. --- @@ -394,34 +635,52 @@ When a field name produces an env key that starts with the command prefix, `scop ### 11.1 `inspect()` as the architectural center -The current architectural direction is that `inspect(ctx: ParseContext)` serves as the canonical parser operation. In this model, it: +The current architectural direction is that `inspect(ctx: ParseContext)` serves +as the canonical parser operation. In this model, it: 1. Accepts a `ParseContext` with input data and structural scope. 2. Walks the parser tree, calling child `inspect()` with derived contexts. -3. Returns a `ParserInfo` containing parse result, help metadata, provenance, child info, and remainder. +3. Returns a `ParserInfo` containing parse result, help metadata, provenance, + child info, and remainder. -`parse()` and `help()` SHOULD derive from `inspect()` (see §4.2). `parse()` and `inspect().result` MUST agree for the same input. Implementations SHOULD NOT maintain separate code paths for parsing vs. introspection. +`parse()` and `help()` SHOULD derive from `inspect()` (see §4.2). `parse()` and +`inspect().result` MUST agree for the same input. Implementations SHOULD NOT +maintain separate code paths for parsing vs. introspection. -> **Transitional architectural direction.** This section describes the clearest current direction of the architecture, supported by direct statements and observed refactors. The exact boundary between "inspect is the workhorse" as a hard implementation requirement vs. a guiding principle is still being established. What is firmly settled: `parse()` results and `inspect()` results MUST be consistent. What is an architectural direction: that `inspect()` is literally the single entry point from which parse and help derive. +> **Transitional architectural direction.** This section describes the clearest +> current direction of the architecture, supported by direct statements and +> observed refactors. The exact boundary between "inspect is the workhorse" as a +> hard implementation requirement vs. a guiding principle is still being +> established. What is firmly settled: `parse()` results and `inspect()` results +> MUST be consistent. What is an architectural direction: that `inspect()` is +> literally the single entry point from which parse and help derive. ### 11.2 `ParseContext` -`ParseContext` carries both input data and structural scope. It MUST flow downward immutably — parents create new contexts for children; contexts MUST NOT be mutated. +`ParseContext` carries both input data and structural scope. It MUST flow +downward immutably — parents create new contexts for children; contexts MUST NOT +be mutated. Current fields: -| Field | Category | Purpose | -|-------|----------|---------| -| `progname: string[]` | Scope | Position in program name hierarchy | -| `path: string[]` | Scope | Position in schema object hierarchy | -| `commands: Record>` | Scope | Sibling commands map | -| `args: string[]` | Input | CLI arguments | -| `values: { name: string; value: unknown }[]` | Input | Config-file values | -| `envs: { name: string; value: Record }[]` | Input | Environment variables | - -`createContext(input?: Input): ParseContext` bridges from user-facing `Input` to internal `ParseContext`, producing empty structural fields. - -> **Transitional.** The six-field structure above describes the current implementation. Whether this exact field set is canonical or subject to evolution is not yet decided. What is settled: ParseContext carries input data and structural scope, and flows downward immutably. What may evolve: the exact set of scope fields, and whether `commands` belongs on the context or on the parser itself. +| Field | Category | Purpose | +| --------------------------------------------------------- | -------- | ----------------------------------- | +| `progname: string[]` | Scope | Position in program name hierarchy | +| `path: string[]` | Scope | Position in schema object hierarchy | +| `commands: Record>` | Scope | Sibling commands map | +| `args: string[]` | Input | CLI arguments | +| `values: { name: string; value: unknown }[]` | Input | Config-file values | +| `envs: { name: string; value: Record }[]` | Input | Environment variables | + +`createContext(input?: Input): ParseContext` bridges from user-facing `Input` to +internal `ParseContext`, producing empty structural fields. + +> **Transitional.** The six-field structure above describes the current +> implementation. Whether this exact field set is canonical or subject to +> evolution is not yet decided. What is settled: ParseContext carries input data +> and structural scope, and flows downward immutably. What may evolve: the exact +> set of scope fields, and whether `commands` belongs on the context or on the +> parser itself. ### 11.3 `ParserInfo` and the info tree @@ -433,18 +692,28 @@ The current `ParserInfo` carries the following base fields: - `remainder: Input` — unconsumed input - `help: HelpInfo` — pre-classified help metadata -These fields MUST be present on every info node. The `result`, `remainder`, and `help` fields are the architectural minimum — they ensure that `parse()` can derive from `inspect().result` and that help can derive from `inspect().help`. +These fields MUST be present on every info node. The `result`, `remainder`, and +`help` fields are the architectural minimum — they ensure that `parse()` can +derive from `inspect().result` and that help can derive from `inspect().help`. -Specialized info types extend `ParserInfo` with type-specific fields. The current observed set: +Specialized info types extend `ParserInfo` with type-specific fields. The +current observed set: -- `FieldInfo`: path, source, sources, required, argument, array, aliases, description, default, boolean +- `FieldInfo`: path, source, sources, required, argument, array, aliases, + description, default, boolean - `ObjectInfo`: `attrs: { [K in keyof T]: ParserInfo }` - `CommandInfo`: name, description, aliases, config, commands - `CommandsInfo`: `commands: { [C in T as C["name"]]: CommandInfo }` - `ProgramInfo`: name, version, `main: ParserInfo` - `ConstantInfo`: value -> **Note.** The exact field set on each info subtype reflects the current implementation. The base fields (`result`, `remainder`, `help`) are architecturally required. The `parser` back-reference and `type` discriminator are strongly evidenced. The specialized fields on each subtype are observed and consistent, but their exact set may evolve — for instance, whether `remainder` belongs on `ParserInfo` or on `ParseResult` has been questioned in transcripts. This specification treats the current shape as provisional. +> **Note.** The exact field set on each info subtype reflects the current +> implementation. The base fields (`result`, `remainder`, `help`) are +> architecturally required. The `parser` back-reference and `type` discriminator +> are strongly evidenced. The specialized fields on each subtype are observed +> and consistent, but their exact set may evolve — for instance, whether +> `remainder` belongs on `ParserInfo` or on `ParseResult` has been questioned in +> transcripts. This specification treats the current shape as provisional. ### 11.4 Help derivation @@ -458,19 +727,34 @@ Each parser MUST build its `help: HelpInfo` field in its `inspect()`: - `inject` → empty help - `constant` → empty help -`format()` reads `info.help` and renders it. Implementations SHOULD NOT use a `classify()` function or `switch (info.type)` dispatcher that reverse-engineers help from info types — each parser is responsible for building its own `help` contribution. +`format()` reads `info.help` and renders it. Implementations SHOULD NOT use a +`classify()` function or `switch (info.type)` dispatcher that reverse-engineers +help from info types — each parser is responsible for building its own `help` +contribution. -> **Note.** An earlier `classify()` approach was explicitly rejected by the project lead: "`HelpInfo` is not really a thing. It should be ObjectInfo and it should just reference ParserInfo. Any type discrimination can happen in the actual format()." The per-parser help-building pattern is the clear intended direction. +> **Note.** An earlier `classify()` approach was explicitly rejected by the +> project lead: "`HelpInfo` is not really a thing. It should be ObjectInfo and +> it should just reference ParserInfo. Any type discrimination can happen in the +> actual format()." The per-parser help-building pattern is the clear intended +> direction. ### 11.5 Invariants Help MUST NOT contain fake path fallbacks (e.g., `|| "field"`, `|| "object"`). -Progname MUST be consistently propagated. `program()` sets `progname: [name]`. `command()` appends its name. Every container MUST propagate progname to children. +Progname MUST be consistently propagated. `program()` sets `progname: [name]`. +`command()` appends its name. Every container MUST propagate progname to +children. -> **Transitional.** The *mechanism* for progname propagation — whether through `ParseContext.progname`, a mutable `Parser.progname` property, or a `ParserInfo.parent` field — has gone through multiple implementations during the project's evolution. The current observed code uses `ParseContext.progname`. This specification requires the invariant (progname is consistently propagated) without mandating the specific mechanism. +> **Transitional.** The _mechanism_ for progname propagation — whether through +> `ParseContext.progname`, a mutable `Parser.progname` property, or a +> `ParserInfo.parent` field — has gone through multiple implementations during +> the project's evolution. The current observed code uses +> `ParseContext.progname`. This specification requires the invariant (progname +> is consistently propagated) without mandating the specific mechanism. -Path MUST be propagated through `ParseContext`. `object()` appends each child's key. Fields use path for CLI option key derivation and env-var key mapping. +Path MUST be propagated through `ParseContext`. `object()` appends each child's +key. Fields use path for CLI option key derivation and env-var key mapping. --- @@ -478,49 +762,81 @@ Path MUST be propagated through `ParseContext`. `object()` appends each child's ### 12.1 Static phased parsing -When both parsers are known at construction time, all help is available before any phase executes. This is the simplest case and MUST be fully supported. +When both parsers are known at construction time, all help is available before +any phase executes. This is the simplest case and MUST be fully supported. ### 12.2 Dependency-generated phases -`inject((dep: D) => Parser)` represents a phase whose parser depends on runtime state. The factory function produces the parser when called with the dependency value. +`inject((dep: D) => Parser)` represents a phase whose parser depends on +runtime state. The factory function produces the parser when called with the +dependency value. -The contract for `inject()` is defined in §8.5. The key phased-parsing implications: +The contract for `inject()` is defined in §8.5. The key phased-parsing +implications: -- When `D` is `void`, the factory MAY be called during `inspect()` (this is how `program()` resolves its config parser and provides full help). -- For non-void `D`, the factory SHOULD NOT be called until the caller provides a legitimate dependency value. Calling the factory with a placeholder or invalid argument to obtain a "preview" parser for help purposes is not acceptable under the current design direction (see §8.5 note on probing). -- After the caller resolves the dependency, the returned parser has baked context and full help is available through standard `inspect()`. +- When `D` is `void`, the factory MAY be called during `inspect()` (this is how + `program()` resolves its config parser and provides full help). +- For non-void `D`, the factory SHOULD NOT be called until the caller provides a + legitimate dependency value. Calling the factory with a placeholder or invalid + argument to obtain a "preview" parser for help purposes is not acceptable + under the current design direction (see §8.5 note on probing). +- After the caller resolves the dependency, the returned parser has baked + context and full help is available through standard `inspect()`. -> **Note.** The now-removed `step()` combinator did call `resume(undefined as never)` to create a preview parser for help. This worked for cases where parser *structure* was dependency-independent. Whether a similar mechanism should be reintroduced as an opt-in pattern (e.g., `inject(fn, { preview: fn(defaultDeps) })`) remains an open design question. +> **Note.** The now-removed `step()` combinator did call +> `resume(undefined as never)` to create a preview parser for help. This worked +> for cases where parser _structure_ was dependency-independent. Whether a +> similar mechanism should be reintroduced as an opt-in pattern (e.g., +> `inject(fn, { preview: fn(defaultDeps) })`) remains an open design question. ### 12.3 What unified help can and cannot mean -Full exact pre-resolution help for a parser whose structure depends on an unresolved dependency is **not achievable**. You cannot enumerate commands contributed by plugins that haven't been loaded. +Full exact pre-resolution help for a parser whose structure depends on an +unresolved dependency is **not achievable**. You cannot enumerate commands +contributed by plugins that haven't been loaded. The spec defines the following help levels: -| Phase type | Pre-resolution help | Post-resolution help | -|-----------|--------------------|--------------------| -| Static | Complete | Complete | -| Enriched values (static structure) | Complete | Complete | -| Static structure, dynamic defaults | Structural preview possible | Complete | -| Dependency-generated structure | Phase 1 only | Complete | -| Recursive nesting | Phase 1 only per level | Complete after all resolutions | +| Phase type | Pre-resolution help | Post-resolution help | +| ---------------------------------- | --------------------------- | ------------------------------ | +| Static | Complete | Complete | +| Enriched values (static structure) | Complete | Complete | +| Static structure, dynamic defaults | Structural preview possible | Complete | +| Dependency-generated structure | Phase 1 only | Complete | +| Recursive nesting | Phase 1 only per level | Complete after all resolutions | -Implementations MUST NOT claim pre-resolution help is complete when it is only partial. If a help formatter encounters an inject boundary, it MUST NOT silently skip it and present the result as complete. +Implementations MUST NOT claim pre-resolution help is complete when it is only +partial. If a help formatter encounters an inject boundary, it MUST NOT silently +skip it and present the result as complete. ### 12.4 Cross-phase help assembly -Cross-phase help assembly — combining help from multiple `inspect()` results — sits **above** `inspect()`. It is the application's responsibility, not a per-parser concern. +Cross-phase help assembly — combining help from multiple `inspect()` results — +sits **above** `inspect()`. It is the application's responsibility, not a +per-parser concern. -After the application resolves a dependency and obtains the phase 2 parser, it calls `inspect()` on that parser. The resolved parser has baked context, so its help includes correct progname and remainder. The application combines phase 1 and phase 2 help. +After the application resolves a dependency and obtains the phase 2 parser, it +calls `inspect()` on that parser. The resolved parser has baked context, so its +help includes correct progname and remainder. The application combines phase 1 +and phase 2 help. -> **Open.** Whether the library should provide a help-accumulation utility (e.g., `mergeHelp(info1, info2)`) or leave this entirely to the application is not yet specified. +> **Open.** Whether the library should provide a help-accumulation utility +> (e.g., `mergeHelp(info1, info2)`) or leave this entirely to the application is +> not yet specified. ### 12.5 Context baking -When `inject()` captures a `ParseContext` and the caller invokes the resolve function, the returned parser MUST have the original context baked in. Calling `parse()` or `inspect()` on the resolved parser MUST use the baked context (progname, path, args, values, envs) without the caller needing to provide them again. +When `inject()` captures a `ParseContext` and the caller invokes the resolve +function, the returned parser MUST have the original context baked in. Calling +`parse()` or `inspect()` on the resolved parser MUST use the baked context +(progname, path, args, values, envs) without the caller needing to provide them +again. -> **Note.** Context baking is the mechanism that makes phased parsing ergonomic — it ensures the resolved parser "remembers" where it sits in the program hierarchy. The exact implementation (closure capture vs. explicit context parameter) is implementation-defined. What the spec requires is the behavioral invariant: the resolved parser behaves as if it has the original context. +> **Note.** Context baking is the mechanism that makes phased parsing ergonomic +> — it ensures the resolved parser "remembers" where it sits in the program +> hierarchy. The exact implementation (closure capture vs. explicit context +> parameter) is implementation-defined. What the spec requires is the behavioral +> invariant: the resolved parser behaves as if it has the original context. --- @@ -528,19 +844,29 @@ When `inject()` captures a `ParseContext` and the caller invokes the resolve fun ### 13.1 Guaranteed inference for literal composition -The following inference behaviors MUST hold without explicit annotation **when parsers are composed using inline literal object maps** (the canonical use case): +The following inference behaviors MUST hold without explicit annotation **when +parsers are composed using inline literal object maps** (the canonical use +case): 1. `object({ key: field(schema) })` MUST infer `Parser<{ key: SchemaType }>`. -2. `commands({ name: { ...object({...}) } })` MUST infer a parser whose type parameter is a union of `Command` with literal `Name` strings. -3. `program({ config: parser })` MUST preserve the config parser's type through to `ProgramType`. +2. `commands({ name: { ...object({...}) } })` MUST infer a parser whose type + parameter is a union of `Command` with literal `Name` strings. +3. `program({ config: parser })` MUST preserve the config parser's type through + to `ProgramType`. 4. `field(schema)` MUST infer `Parser>`. -5. Types inferred at inner composition levels (field → object → commands → program) MUST survive to the outer level without collapsing to `unknown`. +5. Types inferred at inner composition levels (field → object → commands → + program) MUST survive to the outer level without collapsing to `unknown`. -These guarantees apply to the canonical case of literal composition. For computed or dynamically constructed parser maps, see §13.5. +These guarantees apply to the canonical case of literal composition. For +computed or dynamically constructed parser maps, see §13.5. ### 13.2 Extraction helpers -Configliere SHOULD provide type-level extraction helpers that take a parser type (`typeof parser`) as input and extract useful derived types. The convention that extraction helpers operate on *parser types* (not on extracted value types) is established by the project lead: "Everything with *Type should take as a parameter a parser." +Configliere SHOULD provide type-level extraction helpers that take a parser type +(`typeof parser`) as input and extract useful derived types. The convention that +extraction helpers operate on _parser types_ (not on extracted value types) is +established by the project lead: "Everything with *Type should take as a +parameter a parser." The current observed set of helpers includes: @@ -550,46 +876,81 @@ The current observed set of helpers includes: - `ProgramType

` — unwraps `Program` to get `T`. What is normative: -- The convention that extraction helpers operate on parser types, not on extracted value types. [Directly stated by the project lead.] -- Any provided extraction helper MUST produce correct types for the supported composition patterns. + +- The convention that extraction helpers operate on parser types, not on + extracted value types. [Directly stated by the project lead.] +- Any provided extraction helper MUST produce correct types for the supported + composition patterns. What is the current intended direction but may evolve: -- The exact set and naming of helpers listed above is the current observed roster. Future versions may add, rename, or restructure these helpers as the type-level architecture matures. -> **Open.** Whether this set is complete — and in particular whether a `ConfigFileType` or similar helper for config-file authoring shapes is needed — is an unresolved question (see §13.7 and §17). +- The exact set and naming of helpers listed above is the current observed + roster. Future versions may add, rename, or restructure these helpers as the + type-level architecture matures. + +> **Open.** Whether this set is complete — and in particular whether a +> `ConfigFileType` or similar helper for config-file authoring shapes is needed +> — is an unresolved question (see §13.7 and §17). ### 13.3 No hidden defaults -`Parser` MUST NOT default `T` to `unknown` or `any`. A bare `Parser` without a type argument MUST be a compile error. Generic interfaces and functions in the public API MUST NOT default type parameters to `unknown` or `any`. +`Parser` MUST NOT default `T` to `unknown` or `any`. A bare `Parser` without +a type argument MUST be a compile error. Generic interfaces and functions in the +public API MUST NOT default type parameters to `unknown` or `any`. ### 13.4 Forbidden patterns -Public API generic signatures MUST NOT use key-remapping patterns (`[C in T as C["name"]]`) that TypeScript cannot reverse-infer from object literals. +Public API generic signatures MUST NOT use key-remapping patterns +(`[C in T as C["name"]]`) that TypeScript cannot reverse-infer from object +literals. -Type extraction helpers that may produce unions MUST NOT apply non-distributed mapped type transformations (e.g., `{ [K in keyof T]: T[K] }` directly on a union, which collapses to `{}`). +Type extraction helpers that may produce unions MUST NOT apply non-distributed +mapped type transformations (e.g., `{ [K in keyof T]: T[K] }` directly on a +union, which collapses to `{}`). ### 13.5 Computed maps and annotation -When command maps are built dynamically (via `reduce()`, `Object.fromEntries()`, or similar), TypeScript may erase literal key types before `commands()` is called. This is a TypeScript limitation, not a library defect. +When command maps are built dynamically (via `reduce()`, `Object.fromEntries()`, +or similar), TypeScript may erase literal key types before `commands()` is +called. This is a TypeScript limitation, not a library defect. This specification distinguishes two cases: -- **Fully runtime-derived maps** (keys from IO or runtime values): inference produces `unknown`. Explicit type annotation is REQUIRED. This is out of scope for the inference guarantee (§16). -- **Statically computable but TypeScript-widened maps** (keys derivable from static information, but erased by `Object.keys()` or `reduce()`): inference requires explicit annotation, but the annotation burden may be reducible. The `satisfies` operator is the RECOMMENDED pattern. +- **Fully runtime-derived maps** (keys from IO or runtime values): inference + produces `unknown`. Explicit type annotation is REQUIRED. This is out of scope + for the inference guarantee (§16). +- **Statically computable but TypeScript-widened maps** (keys derivable from + static information, but erased by `Object.keys()` or `reduce()`): inference + requires explicit annotation, but the annotation burden may be reducible. The + `satisfies` operator is the RECOMMENDED pattern. -`commands()` MUST infer correctly from whatever type it receives — the limitation is upstream of the library. +`commands()` MUST infer correctly from whatever type it receives — the +limitation is upstream of the library. -> **Open.** The exact boundary between "inference must work" and "annotation required" for statically-computable-but-widened maps is not precisely defined. Whether the library should invest in reducing annotation burden (via identity helpers, `const` generics, or documented patterns) is a product-design choice, not a settled contract question. See §17. +> **Open.** The exact boundary between "inference must work" and "annotation +> required" for statically-computable-but-widened maps is not precisely defined. +> Whether the library should invest in reducing annotation burden (via identity +> helpers, `const` generics, or documented patterns) is a product-design choice, +> not a settled contract question. See §17. ### 13.6 Display quality -TypeScript tooltip display (e.g., showing `CommandsOf<{...}>` instead of the expanded union) is not under Configliere's control. +TypeScript tooltip display (e.g., showing `CommandsOf<{...}>` instead of the +expanded union) is not under Configliere's control. -> **Non-normative.** Display quality is an ergonomic goal the project cares about but cannot guarantee. Implementations SHOULD pursue display improvements where possible, but display quality regressions are not treated with the same severity as inference correctness regressions. +> **Non-normative.** Display quality is an ergonomic goal the project cares +> about but cannot guarantee. Implementations SHOULD pursue display improvements +> where possible, but display quality regressions are not treated with the same +> severity as inference correctness regressions. ### 13.7 Config-file type extraction -> **Open.** `ConfigType

` extracts the parsed runtime output type. For `object()` parsers, this coincides with the config-file authoring shape. For `commands()` parsers, it does not — the parsed output is a `Command` union, while a config file would contain a record keyed by command name. Whether a separate extraction concept is needed for config-file typing is an open design question. See §17. +> **Open.** `ConfigType

` extracts the parsed runtime output type. For +> `object()` parsers, this coincides with the config-file authoring shape. For +> `commands()` parsers, it does not — the parsed output is a `Command` +> union, while a config file would contain a record keyed by command name. +> Whether a separate extraction concept is needed for config-file typing is an +> open design question. See §17. --- @@ -597,19 +958,25 @@ TypeScript tooltip display (e.g., showing `CommandsOf<{...}>` instead of the exp ### 14.1 Parse failure -When a field fails validation, `inspect()` MUST produce a `Fail` result with the validation error. `ObjectValidationError` aggregates child failures with their paths. +When a field fails validation, `inspect()` MUST produce a `Fail` result with the +validation error. `ObjectValidationError` aggregates child failures with their +paths. ### 14.2 Command mismatch -When no command matches the input, `commands()` MUST produce a `NoCommandMatchError` listing available commands. +When no command matches the input, `commands()` MUST produce a +`NoCommandMatchError` listing available commands. ### 14.3 Help on failure -When `program()` detects `--help`, it MUST produce help text even if the underlying parse would fail. The help/version screening occurs before config parsing. +When `program()` detects `--help`, it MUST produce help text even if the +underlying parse would fail. The help/version screening occurs before config +parsing. ### 14.4 Error provenance -Validation errors SHOULD include the field path and the failing source's identity where possible. +Validation errors SHOULD include the field path and the failing source's +identity where possible. --- @@ -617,43 +984,76 @@ Validation errors SHOULD include the field path and the failing source's identit ### 15.1 Parser composition as the extension mechanism -New functionality SHOULD be added through parser composition — new combinator functions that return `Parser` — not through special-case hooks or configuration flags. +New functionality SHOULD be added through parser composition — new combinator +functions that return `Parser` — not through special-case hooks or +configuration flags. ### 15.2 Phased/injected growth -Multi-phase parsing MUST remain compositional. New phases are added by nesting `inject()` inside existing parsers. There MUST NOT be a global phase registry or orchestration layer. +Multi-phase parsing MUST remain compositional. New phases are added by nesting +`inject()` inside existing parsers. There MUST NOT be a global phase registry or +orchestration layer. ### 15.3 Command extension -New commands are added by extending the map passed to `commands()`. The type system reflects the extension through union widening. +New commands are added by extending the map passed to `commands()`. The type +system reflects the extension through union widening. ### 15.4 Schema enrichment -Configliere delegates field validation to Standard Schema. New schema libraries can be adopted without changes to Configliere, as long as they implement the Standard Schema specification. +Configliere delegates field validation to Standard Schema. New schema libraries +can be adopted without changes to Configliere, as long as they implement the +Standard Schema specification. ### 15.5 Constraint against special cases -Implementations SHOULD NOT introduce behaviors that only work for specific parser types (e.g., help features that only work with `program()` but not with standalone `commands()`). When special behavior is needed, it SHOULD be expressed through the type system (e.g., `D = void` making `inject()` resolvable during inspect). +Implementations SHOULD NOT introduce behaviors that only work for specific +parser types (e.g., help features that only work with `program()` but not with +standalone `commands()`). When special behavior is needed, it SHOULD be +expressed through the type system (e.g., `D = void` making `inject()` resolvable +during inspect). --- ## 16. Explicit Non-Goals -The following are not supported by the current architecture. Each is graded by the strength of evidence for its exclusion. +The following are not supported by the current architecture. Each is graded by +the strength of evidence for its exclusion. -1. **Env-var-based command selection.** No `COMMAND=serve` mechanism exists. Commands are selected by CLI arguments only. [Not implemented; never discussed as a possibility.] +1. **Env-var-based command selection.** No `COMMAND=serve` mechanism exists. + Commands are selected by CLI arguments only. [Not implemented; never + discussed as a possibility.] -2. **Config-file-based command selection.** This was implemented and explicitly removed by the project lead. [Directly rejected — the strongest evidence for any non-goal in this list.] +2. **Config-file-based command selection.** This was implemented and explicitly + removed by the project lead. [Directly rejected — the strongest evidence for + any non-goal in this list.] -3. **Exact inference for fully runtime-derived maps.** When keys come from runtime values (e.g., `Object.fromEntries(runtimeKeys.map(...))`), TypeScript erases literal types before Configliere's API is called. This is a TypeScript limitation, not a library defect. [Outside the supported model — not a design rejection, but a platform boundary.] +3. **Exact inference for fully runtime-derived maps.** When keys come from + runtime values (e.g., `Object.fromEntries(runtimeKeys.map(...))`), TypeScript + erases literal types before Configliere's API is called. This is a TypeScript + limitation, not a library defect. [Outside the supported model — not a design + rejection, but a platform boundary.] -4. **Exact pre-resolution help for dependency-generated parser structure.** A parser whose commands depend on unresolved runtime state cannot enumerate those commands before resolution. [Architectural impossibility — not a design choice, but a logical consequence of the phased model.] +4. **Exact pre-resolution help for dependency-generated parser structure.** A + parser whose commands depend on unresolved runtime state cannot enumerate + those commands before resolution. [Architectural impossibility — not a design + choice, but a logical consequence of the phased model.] -5. **Display-quality guarantees for TypeScript tooltips.** TypeScript controls how type aliases are displayed in editor tooltips. Configliere cares about display quality as an ergonomic goal but cannot guarantee it. [Outside the library's control — not a rejection, but a platform limitation.] +5. **Display-quality guarantees for TypeScript tooltips.** TypeScript controls + how type aliases are displayed in editor tooltips. Configliere cares about + display quality as an ergonomic goal but cannot guarantee it. [Outside the + library's control — not a rejection, but a platform limitation.] -6. **Structure-only introspection without input.** `inspect()` requires a `ParseContext`. There is currently no "describe your structure without parsing" operation. [Not implemented; not requested in transcripts. Whether it *should* be supported is an open question (§17), but it is not currently offered.] +6. **Structure-only introspection without input.** `inspect()` requires a + `ParseContext`. There is currently no "describe your structure without + parsing" operation. [Not implemented; not requested in transcripts. Whether + it _should_ be supported is an open question (§17), but it is not currently + offered.] -7. **Automatic help accumulation across phases.** Cross-phase help assembly is currently the application's responsibility. [Not implemented. Whether the library should provide a utility is an open question (§17), but no such utility currently exists.] +7. **Automatic help accumulation across phases.** Cross-phase help assembly is + currently the application's responsibility. [Not implemented. Whether the + library should provide a utility is an open question (§17), but no such + utility currently exists.] --- @@ -661,37 +1061,65 @@ The following are not supported by the current architecture. Each is graded by t The following areas are unresolved and MUST NOT be silently treated as settled: -1. **Provenance preservation of original env keys after scoping.** After `scope()` transforms env keys, the original key (e.g., `SERVE_PORT`) is lost. This conflicts with the project's inspectability emphasis. Whether to track pre-scoping identity is an open design question. +1. **Provenance preservation of original env keys after scoping.** After + `scope()` transforms env keys, the original key (e.g., `SERVE_PORT`) is lost. + This conflicts with the project's inspectability emphasis. Whether to track + pre-scoping identity is an open design question. -2. **Nested command env semantics.** Whether `API_USERS_PORT` should reach a `port` field inside `api > users` is mechanically plausible but untested and undiscussed. The spec does not guarantee specific nested-command env behavior. +2. **Nested command env semantics.** Whether `API_USERS_PORT` should reach a + `port` field inside `api > users` is mechanically plausible but untested and + undiscussed. The spec does not guarantee specific nested-command env + behavior. -3. **Config-file authoring type extraction.** `ConfigType

` extracts the parsed output type, not the config-file authoring shape. For commands, these differ. Whether a separate `ConfigFileType

` or equivalent is needed is unresolved. +3. **Config-file authoring type extraction.** `ConfigType

` extracts the + parsed output type, not the config-file authoring shape. For commands, these + differ. Whether a separate `ConfigFileType

` or equivalent is needed is + unresolved. -4. **Computed-map inference boundary.** Where "inference must always work" stops and "annotation required" begins is not precisely defined for statically-computable-but-TypeScript-widened maps. +4. **Computed-map inference boundary.** Where "inference must always work" stops + and "annotation required" begins is not precisely defined for + statically-computable-but-TypeScript-widened maps. -5. **Scoped-wins ordering guarantee.** Whether `scope()` must guarantee that command-prefixed env vars always beat global env vars of the same derived key, regardless of insertion order, is not yet normative. +5. **Scoped-wins ordering guarantee.** Whether `scope()` must guarantee that + command-prefixed env vars always beat global env vars of the same derived + key, regardless of insertion order, is not yet normative. -6. **Prefix collision handling.** Whether `scope()` should be made schema-aware to avoid false prefix matches on field names is unresolved. +6. **Prefix collision handling.** Whether `scope()` should be made schema-aware + to avoid false prefix matches on field names is unresolved. -7. **`ParseContext` field set.** Whether the current six-field `ParseContext` structure is canonical or subject to evolution is not yet decided. +7. **`ParseContext` field set.** Whether the current six-field `ParseContext` + structure is canonical or subject to evolution is not yet decided. -8. **Help accumulation API.** Whether the library provides a utility for combining help from multiple `inspect()` results across phases is not specified. +8. **Help accumulation API.** Whether the library provides a utility for + combining help from multiple `inspect()` results across phases is not + specified. -9. **Progname propagation mechanism.** The most recent observed code uses `ParseContext.progname`. Whether a mutable `Parser.progname` property or a `ParserInfo.parent` field also participate is an implementation detail that may evolve. +9. **Progname propagation mechanism.** The most recent observed code uses + `ParseContext.progname`. Whether a mutable `Parser.progname` property or a + `ParserInfo.parent` field also participate is an implementation detail that + may evolve. -10. **Command-scoped help dispatch.** Whether `commands.help()` should match against args and delegate to the matched command's help, or always show the top-level listing, is unresolved. +10. **Command-scoped help dispatch.** Whether `commands.help()` should match + against args and delegate to the matched command's help, or always show the + top-level listing, is unresolved. -11. **`toSnake` dependency.** Env-key derivation currently depends on the `ts-case-convert` library for `toSnake` conversion. Whether the spec should define conversion rules independently is open. +11. **`toSnake` dependency.** Env-key derivation currently depends on the + `ts-case-convert` library for `toSnake` conversion. Whether the spec should + define conversion rules independently is open. --- ## 18. Normative Test Agenda -The following test families MUST be maintained. This section names required test categories, not exhaustive test cases. +The following test families MUST be maintained. This section names required test +categories, not exhaustive test cases. Each test item is marked by purpose: -- **[Verify current]** — confirms behavior observed working in the current implementation. -- **[Lock down contract]** — asserts an intended invariant that this specification treats as normative. + +- **[Verify current]** — confirms behavior observed working in the current + implementation. +- **[Lock down contract]** — asserts an intended invariant that this + specification treats as normative. ### 18.1 Source precedence @@ -703,7 +1131,8 @@ Each test item is marked by purpose: ### 18.2 Source provenance -- `FieldInfo.source` and `FieldInfo.sources` are always populated [Lock down contract] +- `FieldInfo.source` and `FieldInfo.sources` are always populated [Lock down + contract] - source types are correct [Verify current] - the winning source is identified [Lock down contract] - invalid sources are not displayed in help output [Verify current] @@ -745,8 +1174,10 @@ Each test item is marked by purpose: ### 18.7 `inspect()` invariants -- `parse()` results and `inspect().result` agree for the same input [Lock down contract] -- `help()` output and `format(inspect())` agree for the same input [Lock down contract] +- `parse()` results and `inspect().result` agree for the same input [Lock down + contract] +- `help()` output and `format(inspect())` agree for the same input [Lock down + contract] - every `ParserInfo` has result, remainder, and help [Lock down contract] - container parsers embed child info [Verify current] - `FieldInfo` always has source and sources [Lock down contract] @@ -761,19 +1192,33 @@ Each test item is marked by purpose: ### 18.9 Type inference Compile-time type tests: + - `object()` infers exact value type from literal map [Lock down contract] -- `commands()` infers exact command union with literal name strings [Lock down contract] +- `commands()` infers exact command union with literal name strings [Lock down + contract] - `ConfigType` extracts `T` from `Parser` [Lock down contract] - `CommandType` extracts named command member [Lock down contract] - `ProgramType` extracts inner config type [Lock down contract] -- `commands()` does not collapse to `Command` [Lock down contract] +- `commands()` does not collapse to `Command` [Lock down + contract] - `ConfigType` on command union does not collapse to `{}` [Lock down contract] - bare `Parser` without type argument fails to compile [Lock down contract] ### 18.10 Helper extraction -Tests MUST verify that provided extraction helpers (currently `ConfigType`, `CommandType`, `CommandsType`, `ProgramType`) extract correct types from parser instances. [Lock down contract — for whatever helpers exist at the time.] If the helper set evolves, tests MUST cover whatever helpers are provided. +Tests MUST verify that provided extraction helpers (currently `ConfigType`, +`CommandType`, `CommandsType`, `ProgramType`) extract correct types from parser +instances. [Lock down contract — for whatever helpers exist at the time.] If the +helper set evolves, tests MUST cover whatever helpers are provided. --- -*This specification is derived from reverse-engineering research across 13 transcript sessions and current library source code. Items marked **Open** or **Transitional** represent areas where the evidence does not support firm normative language. Interface snippets labeled **Illustrative** show the current observed shape rather than an exact permanent contract. Evidence-graded notes throughout distinguish settled contract from current intended direction and provisional interpretation. The specification aims to be strong enough to guide implementation and testing while remaining honest about what is established and what is still being determined.* +_This specification is derived from reverse-engineering research across 13 +transcript sessions and current library source code. Items marked **Open** or +**Transitional** represent areas where the evidence does not support firm +normative language. Interface snippets labeled **Illustrative** show the current +observed shape rather than an exact permanent contract. Evidence-graded notes +throughout distinguish settled contract from current intended direction and +provisional interpretation. The specification aims to be strong enough to guide +implementation and testing while remaining honest about what is established and +what is still being determined._ From 6a5a97eea5b8d3ff7bf296c9ff1a4604264077f9 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Fri, 3 Apr 2026 11:04:55 -0400 Subject: [PATCH 3/3] Format SVG images for Deno 2.7.11 compatibility --- images/configliere-after.svg | 541 +++++++++++++++++++++++++--------- images/configliere-before.svg | 362 +++++++++++++++++------ 2 files changed, 678 insertions(+), 225 deletions(-) diff --git a/images/configliere-after.svg b/images/configliere-after.svg index 746cc3e..657d558 100644 --- a/images/configliere-after.svg +++ b/images/configliere-after.svg @@ -7,15 +7,23 @@ width="3268.8571428571427" height="1156" > - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CLICLI + + + + + + + + + ENVENV + + + + + + + + + Config Files...Config Files... + + + + + + + + + + + + + + + + + + + + + + + Parse/ValidateParse/Validate + + + + + + + + + + + + + + + + + + + + + + + Parse/ValidateParse/Validate + + + + + + + + + + + + + + + + + + + + + + + Parse/ValidateParse/Validate + + + + + + + + + MergeMerge + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Config CConfig C + + + ConfigliereConfigliere + + + + + + + + + C.xC.x + + + + + + + + + C.zC.z + + + + + + + + + C.yC.y + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Program + >Program + diff --git a/images/configliere-before.svg b/images/configliere-before.svg index 84eeba1..48878e2 100644 --- a/images/configliere-before.svg +++ b/images/configliere-before.svg @@ -7,10 +7,12 @@ width="2938.7142857142926" height="1690.1428571428376" > - + + + + + + + + + + + + + + + + + Args.xArgs.x + + + + + + + + + c.zc.z + + + + + + + + + yy + + + + + + + + + + + + + + + + + + + + + + + Parse ArgsParse Args + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ProgramProgram + + + + + + + + + CLICLI + + + + + + + + + ENVENV + + + + + + + + + + + + + + + + + + + + + + + y = proces.env.Yy = proces.env.Y + + + + + + + + + + + + + + + + + + + + + Config Files...Config Files... + + + + + + + + + + + + + + + + + + + + + + + json = read("c.json") + >json = read("c.json") +