Project Tom Hagen: rebuild Configliere as a typed request router - #20
Merged
Merged
Conversation
Document the layered claim protocol, Token model, Prefix/path semantics, ParseContext architecture, primitives walk, and parse chart format. Resolves the circularity in PR #15 and unifies args/values/envs addressing. Removes sequence() in favor of inject(); commands() moves to tuple-array form for TypeScript inference.
Eight-phase plan that lands new types, replaces field() with option/argument/constant/many/passthrough, rewrites object/commands/program over the claim protocol, removes sequence(), adds parse-chart renderer.
- New types: Token (range/path/name addresses), Prefix (per-channel), AvailableInput (parser-visible view). - ParseContext rewritten: removes path/commands/args/values/envs leaks; adds prefix/input/available. - ParserInfo, Done, Fail: claims field, AvailableInput remainder. - createContext + toAvailable produce stable-addressed AvailableInput from raw Input. - subtractArgs / emptyAvailable / isEmpty helpers. Existing parsers fail to type-check until phases 2-8 catch up.
- option<T>: named claim grammar across args/values/envs derived from Prefix; first-match-only on args; --foo/--no-foo/--foo=val/--foo VAL/-a forms; aliases at args-path top only; cli > env > value priority. - argument<T>: first non-dash positional, CLI-only. - constant<T>: empty grammar, returns fixed value. - many<T>: repeats wrapped parser's args grammar; args-only. - passthrough(): claims `--` sentinel + everything after. - source<T> helpers: noneSource, defaultSource, resolve. - schema.isBoolean export. 24/24 tests pass.
- Children evaluated in declaration order; each receives the previous child's args remainder. - Per-child Prefix extension: values + key, envs + UPPER + "_", args + key. - scopeForChild projects values entries to subtree at key. - envs entries pass through; option computes env name from prefix.envs. - Object itself emits no claims (compound orchestrator). 7/7 object tests pass; 31/31 across all phases so far.
…ocol
- API: commands([["name", parser], ...], { default? }) — tuple-array form
for TypeScript inference of result union types.
- Selector grammar: scans past leading non-positional tokens to find first
positional matching a command name.
- Dispatch-then-delegate: hands inner all input minus the selector token;
no positional barrier (pre and post-selector both reach inner).
- Path: extends prefix.values + prefix.envs with name; resets prefix.args.
- Scopes values by command name; scopes envs by command prefix.
9/9 commands tests pass; 40/40 across phases 1-4.
- Claims --help/-h; claims --version/-v iff a version string is provided.
- Delegates to inner parser with its own claims stripped from available.
- Result shape: { help: boolean, version: boolean, config: T } —
flat booleans alongside inner's value (per spec choice to avoid
discriminated-union short-circuiting).
- Does not claim '--' (passthrough()'s job).
44/44 tests across phases 1-5.
- inject's parser-factory now uses input/available threading; resolve() rebuilds available via toAvailable when handed a new Input. - Tests updated to new context API. - No sequence() references found anywhere; nothing to remove. 46/46 tests across phases 1-6.
- chart(info, input) renders three sections: input, parse, remainder. - Each parser node shows its claims with addresses + dereferenced content (args slice, value path, env name). - ⊘ marks parsers with no own claims. - Walks attrs (object), commands (commands), and main (program) shapes. 49/49 tests across phases 1-7.
- Delete: lib/field.ts, lib/parse-args.ts, lib/remaining.ts, lib/lens.ts, test/field.test.ts, test/lens.test.ts. - Update lib/mod.ts: export new option/argument/many/passthrough/chart; remove field export. - Update lib/help.ts: inline optionKey (parse-args.ts is gone). - Port test/boolean.test.ts and test/help.test.ts to new primitives; drop tests for behaviors removed in the new model (argument from env/value sources). - Port examples/simple.ts, examples/commands.ts, examples/phased-commands.ts to option/argument and tuple-form commands. Final: 51/51 tests pass, deno check + lint clean. Known follow-ups (pre-existing from earlier phases): - CLI number coercion for non-boolean options (was via parse-args.ts' primitive(); not yet replaced). - commands help.commands list not populated when no name matches.
- option matchArgs: coerce non-boolean string values via shared coerceValue (schema-aware: only coerces to number if schema accepts it); --port 4000 now resolves to 4000:number instead of falling back to default. - commands inspect: gather metadata for all command entries (not only the matched one) so help.commands lists the full registry; program --help now renders the Commands section. Adds regression tests for both.
- New available.subtract(av, claims) handles all three token types: removes arg indices, prunes value tree at claimed paths, deletes env names from env source records. - All leaves (option/argument/passthrough/many) and object orchestration use subtract for remainder computation. - object no longer incorrectly scopes values for children via scopeForChild — children now receive the full value tree and use their prefix.values path to navigate, matching how envs work. This also fixes value reading in object, which was previously broken. - Sibling parsers now see a filtered available — same source/path cannot be claimed twice. Conservation invariant (claims ∪ remainder == available) now holds across all three channels. Adds tests covering value-path pruning, nested paths, env name removal, and sibling double-claim prevention.
§2.1 step 4 and §5 now state every parser's claims is the aggregate of own and descendants' claims, with conservation holding at every level (claims ∪ remainder == available).
- New internal lib/parser.ts: defineParser({type, claim, parse}) builds
Parser<T> with auto parse/help shims and envelope construction;
computes remainder via subtract, calls user-supplied phases.
- Parser<T> gains .claim(ctx) for cheap claim-only traversal by compounds.
- ParseContext.read(token) overloaded by token type:
arg → string[], value → unknown, env → string. Closure over original
input; same read flows through every derived context via spread.
Existing parsers do not yet implement claim and will fail type-check
until they are refactored in the next commit.
- Five leaves (option, argument, constant, passthrough, many) and four
compounds (object, commands, program, inject) now build via the
internal defineParser({type, claim, parse}) wrapper. Wrapper supplies
parse/help/claim shims and the {type, parser, prefix, claims,
remainder} envelope; user supplies a claim phase (Token[]) and a
parse phase (extras).
- Compounds aggregate descendants' claims via recursive child.claim()
for the cheap pass and call child.inspect() in their parse phase to
materialize result envelopes.
- option's matchArgs now returns just {token: Token}; a new
extractOptionValue(schema, prefix, slice) reconstructs the value
from ctx.read(token) output. No pre-extracted values in claim
outputs.
- inject overrides its inner's parse/help with a freshCtx that re-runs
createContext on the new input, ensuring ctx.read closes over the
current input rather than the captured one.
60/60 tests pass; deno check + deno lint clean; smoke tests
(examples/commands.ts dev/--help) verified.
Help becomes a regular subcommand instead of a flag claim, sidestepping the --help disambiguation problem. Introduces ParseScope (Effection-style typed contextual values), a CommandsScope set by commands during dispatch, and a new command(name, parser, opts?) factory replacing the tuple-array form. --help/--version stay as program options for now.
commit: |
This was referenced Aug 21, 2026
This was referenced Sep 3, 2026
Collaborator
Author
cowboyd
marked this pull request as ready for review
September 4, 2026 02:15
This was referenced Sep 5, 2026
cowboyd
force-pushed
the
project-tom-hagen
branch
from
September 9, 2026 01:21
ad0a988 to
11b186e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
This begins a ground-up rebuild of Configliere. Its previous parser abstraction coupled token consumption, configuration binding, subcommands, and controls until neither the runtime behavior nor the inferred types were easy to reason about.
Most CLI parsers treat an invocation as a bag of arguments attached to a command tree. Configliere instead needs to identify where and how the caller intends to enter the application, and then provide the exact validated model for that entry point.
Approach
Treat a command-line program as a tree of application entry points. Each route has a local model and explicitly supported methods—
HELP,VERSION, orEXECUTE. Parsing resolves the pair(method, route)into an intent rather than returning an undifferentiated bag of options.Route discovery precedes parameter binding. Command words therefore select routes instead of competing with positional values, and parameters bind only within the segment owned by their route. An execute intent exposes the selected route's model alongside a path-addressed map of every model in its matched lineage.
The definition DSL is immutable function composition: each element enriches both a plain runtime route definition and its static type. TypeScript can consequently derive a flat discriminated union of every reachable intent, while Standard Schema remains the boundary for validating individual model values. The implementation should be read as an interpreter over this route definition, not as a CLI framework that owns dispatch or application control flow.