Skip to content

Project Tom Hagen: rebuild Configliere as a typed request router - #20

Merged
cowboyd merged 41 commits into
mainfrom
project-tom-hagen
Sep 23, 2026
Merged

cowboyd merged 41 commits into
mainfrom
project-tom-hagen

Conversation

@cowboyd

@cowboyd cowboyd commented Aug 21, 2026 •

Copy link
Copy Markdown
Collaborator

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, or EXECUTE. 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.

cowboyd added 24 commits May 4, 2026 16:09
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.
@pkg-pr-new

pkg-pr-new Bot commented Aug 21, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/configliere@20

commit: 11b186e

@cowboyd

cowboyd commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator Author

Stack continuation: #20 (typed request-router foundation) → #22 (dynamic phases) → #23 (source-aware binding). Each downstream PR is based on the preceding branch.

@cowboyd
cowboyd merged commit 6c82188 into main Sep 23, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant