Conversation
An evaluation spike, not adoption. The released `xmd` command definitions and dispatch boundary are re-expressed through the route API proposed in bombshell-dev/configliere#30, so the diff, the focused regressions and the packaging results can say what that API can and cannot carry. `packages/cli/src/cli-route.ts` owns the immutable definitions, the synchronous parse driver and the presentation that keeps help and version output identical. `cli.ts` dispatches on `intent.method` and `intent.route` and reads each handler's model off the matching route. `workflow.ts` gives up its legacy definition; `props.ts` gives up the released parser's inspection and owns the source precedence walk the proposed API has no equivalent for. Three limits shaped the port and stay visible in it: - A repeatable option cannot be read from argv at all: the binding loop truncates a reader's view at the first unclaimed word, and a reader settles its parameter once. `--include` and `--pattern` are lifted out of argv and handed back as route value sources. - `checkpoint()` adds values, never parameters or routes, so a document's generated `--props-*` options are still lifted before parsing. The two parses that remain are named in the code as the checkpoint gap. - The preview is an HTTPS tarball. Deno reports `Not implemented scheme 'https'`, so the exact tarball is mirrored unchanged under `packages/cli/vendor/configliere-pr30/` and Deno resolves it through a local `links` override. Node and Bun consume the pnpm tarball. Every scanner that survives is a scanner the route API cannot replace, and each one now carries the reason.
Configliere PR #32 adds `multiple()`, which is the facility the previous preview could not express. The binding loop now shows a multiple parameter the whole phase instead of truncating its view at the first unclaimed word, and the reader returns every occurrence in the order it was written — so `--include` and `--pattern` bind from argv on the route that declares them. That retires the whole workaround this spike needed for them: `readRepeatedOption`, its interfaces, the lifting inside `liftArgs`, the `liftedValues`/`routeValues` value-source channel, and the extra parameter `test` took because a model could not say what a scanner had read. `--pattern` keeps no schema default, because `xmd test` needs to know whether the caller wrote one: a pattern written against a single document is refused, and a default is not a refusal. The glob help displays is carried beside the schema instead, and the command applies `DEFAULT_PATTERN` itself. The preview mirror moves to the exact PR #32 bytes, `0.4.0-pr+08b3a6835a0c8be9c7b946e26f0e4f0706155964`. CFE7 changes with its subject: it asserted that a repeatable option had to arrive as a value source, and now asserts that argv binds it, including the occurrence written after the document that no reader could previously see.
PR #32 exports `dynamic`, so the phase `checkpoint()` is built from is reachable at last. Measured against the published bytes, it does everything the props migration needs — at runtime. It adds a document's generated options after inspection, binds one written before the phase that declares it, introduces nested routes to arbitrary depth, and reports a failed load through the ordinary issue path. What it cannot do is describe any of that. When the resolver's element list is derived from the run — which a document's declared properties always are — the parse type collapses to a fully resolved union: the increment is absent from it, `resume` types as `unknown`, and the model describes the phase after the boundary rather than the one before it. An explicit return annotation does not restore it, and the same definition with a statically known element list types exactly, so the limit is specific to a list only the run knows. Driving it would therefore take a cast, which this repository does not allow and which would hide the finding. So the properties phase keeps preparing argv before parsing, and the two parses stay — but the comment on them now names the real reason, which is typing rather than a missing capability. CFE4 keeps the checkpoint's own limit. CFE4b is new and holds the rest: it drives a dynamic phase by parsing every step back out of a value whose type stopped describing it, which is the finding made executable. It also records the ordering that defeated my first reading of it — an argument in the earlier phase claims the word before the route it names exists, so a dynamic route is unreachable behind a positional, and reachable without one.
pnpm resolves the preview for Node and Bun, and its lock still named the PR #30 URL. The entry carries no integrity hash either way: pnpm records none for an HTTPS tarball dependency, which is why this spike keeps its own SHA-512 of the bytes it consumed.
This branch has not been deployed
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.
Closes nothing. Evaluates bombshell-dev/configliere#30
and then bombshell-dev/configliere#32.
Why
Configliere PR #30 proposes replacing the released
program()/commands()/object()/field()parser with a statically typed entry-point router. #796 asks whether the real
xmdcommand definitions and dispatch boundary can be expressed through it, andwhat that would cost. The only way to answer is to write it.
The artifact this evaluates
configliere@0.4.0-pr+dbd7d191ab8aff37c71cc03bc4688ad372ec7e23fb52bc6567dadd2528e684b123d013eb9b701ace7542739864076c30f3fc0657d0b2678c2e90d2317cf1e9ad5bbbabb457ca8ad7365f232eef209b0e0b3a68532cc3447491d0ccf2a9c3b153db1ce7cc1df477a9pnpm-lock.yamlrecordsresolution: {tarball: https://pkg.pr.new/configliere@30}with
version: 0.4.0-prand no integrity hash — pnpm records none for anHTTPS tarball, and the recorded version drops the
+dbd7d19…build metadata.The URL is the only identity in the lock and it is mutable upstream, so the
SHA-512 above is this evaluation's own record of the bytes it consumed.
Recommendation
Request upstream changes and repeat. Three blockers stand between this
preview and an adoption decision. None of them is an XMD defect, and none is
fixable on this side.
Not implemented scheme 'https'. The only configuration that gives Deno boththe runtime and the types is a
linksoverride onto an unpacked mirror, andthat is refused by every
--node-modules-dir=nonetask the repository runs:build:web,build,verify,verify:clean. Publishing the previewsomewhere Deno resolves — JSR, or a real npm prerelease tag — removes this
entirely.
reader and not by an XMD-written one.
checkpoint()adds valuesand nothing else, so a document's generated
--props-*options still have tobe lifted out of argv before parsing — which is the phase the dynamic API
exists to remove.
Everything else the port met, and the static shape is genuinely better than what
it replaces.
What changes
Before: one
program()with a flatcommands()map; the parser ignoresevery option it does not define;
xmd.parse()is called four times in differentplaces; the command is a string on a config object;
props.tsreads per-sourceprovenance out of the parser's
inspect().After: a route tree whose entry points are addresses.
parse()returns anintent, dispatch narrows on
intent.methodandintent.route, and each handlerreads a model that belongs to its own route. Observable CLI behaviour is
unchanged except where this description says otherwise.
How it works
packages/cli/src/cli-route.tsowns the immutable definitions, the synchronousparse driver and the presentation.
cli.tsowns I/O, Effection lifetime andevery refusal that has an accepted wording. No Configliere callback performs
I/O; no generator uses
async/await.Two definitions, not one. Configliere selects a child route from any
unclaimed word in the segment;
xmdselects a command only from the firsttoken, and a word anywhere else is a document reference.
commandToken()chooses between the full tree and a children-less shorthand root, which is what
keeps
xmd --raw run -e '# Probe'naming a document calledrun.Review guide
Start with:
packages/cli/src/cli-route.tsThen review:
commandToken()/definitionFor— the first-token rule.dispatch()inpackages/cli/src/cli.ts— the narrowing and the order ofrefusals before a parse failure is reported.
liftArgs()/readRepeatedOption()/readDocumentArguments()— everythingthe grammar cannot bind, and why.
resolveProps()inpackages/cli/src/props.ts— the precedence walk thatreplaced the released parser's inspection.
Look carefully at: the six places where an accepted message had to be
restored ahead of the stock diagnostic. Each is a case where the proposed API
refuses something the released one ignored.
What must stay true
commandToken()and by parsing a children-less definition otherwise; checked by CFE1 and by
inline-cli.test.tsIE31.--version, and it writes the bare version.Enforced by declaring
version()on the root alone; checked by CFE5,cli-help.test.tsCH4 andupgrade-cli.test.tsUC6.--still protects a positional. Enforced by readingintent.literals;checked by CFE6,
stdin-cli.test.tsSI15 andworkflow-cli.test.tsWFC13.running each command's own scan before the parse failure is reported; checked
by UC6/UC7/UC11, SD12, CA7, DT11 and WFC8.
by
beforeProperties()truncation plusextractPropsArgs; checked byprops-cli.test.tsandprops-sources.test.ts.How to verify it
packages/cli/tests/configliere-route-api.test.tsproves the definitions andthe intent typing (CFE1–CFE8, CFE11, CFE13). It is a production-boundary
test: it exercises the real tree, not one written for the occasion. Mutating
commandToken()to accept a command in any position fails CFE1 and nothingelse, which is the discrimination it was written for.
cli-help.test.ts(15 steps) andplan-cli.test.tsPS4 prove help output isbyte-identical, including
--agent-provider <AGENTPROVIDER>and the trailing-h, --help show helprow.deno task test packages/cli/tests/— 93 files pass (712 steps), 2 fail.Both failures are
Could not resolve 'npm:configliere@^0.4.0-pr'in a childprocess the suite sandboxes with its own
HOME;agent-clipasses onceDENO_DIRis inherited,workflow-crashWFX3 does not. Neither is a parsingdifference — see Risks.
cli-helpCH7, which requires emptystderr while the tsx child writes
[DEP0205] module.register() is deprecatedbefore any CLI code runs. Bun runs the same four files 69/69.
deno task lint,deno task check,deno task check:jsrandgit diff --checkall exit 0.Scope
Included
the proposed API.
Intentionally unchanged
packages/cli/src/workflow.tskeeps lifecycle request semantics and itsEffection
Result<WorkflowCommand>; only its legacy definition moved.packages/cli/src/props.tskeeps schema inspection, lossless decoding andsource diagnostics; only its resolver changed.
enumerates every positional to prove it.
New abstractions
cli-route.tsexists because the definitions, the parse driver and thepresentation are one subject and were previously spread across
cli.tsandworkflow.ts. Consumers:cli.tsand the acceptance test.withDefault()exists becauseschema()acceptsStandardSchemaV1<T, T>,so every Zod
.default()is rejected — the default widens the input typewhile the output stays
T. It also records whether help should describe theparameter as required, which is the one thing the released
field.default()expressed and this API does not.
routeValues()exists because a lifted repeatable option has to reach themodel addressed to the route that owns it.
New dependencies
configliereathttps://pkg.pr.new/configliere@30(
0.4.0-pr+dbd7d191ab8aff37c71cc03bc4688ad372ec7e23), replacing^0.4.0.contain the API under evaluation.
Generated or mechanical changes
packages/cli/vendor/configliere-pr30/is the exact preview tarball, unpackedand unmodified — 247 files, 8 559 lines, 1.2 MB. Verified byte-identical to
the pnpm store copy with
diff -r. It exists only because Deno cannot resolvean HTTPS tarball dependency. Skim it; nothing in it was written here. It is
excluded from
deno.json's workspaceexclude, from the lint ignore list andfrom
.oxfmtrc.json, exactly as the other vendored packages are. The tarballships no LICENSE file; its manifest declares MIT.
cli-route.ts+966,cli.ts+648/−514, the new test +400,props.ts+44/−79,
workflow.ts−69, and 24 lines of manifest and lock changes.What the port had to work around
Each of these is a finding, not a shim. Each one is named in the code.
bindPhasetruncates every reader's view at the first unclaimed word, so an occurrence written after a value is invisible; a reader settles its parameter on first success; andCLIReadtypes the valuestring | boolean, so a list cannot leave a reader anyway.--includeand--patternare lifted out of argv byreadRepeatedOption()and handed back as route value sources.--includehad no scanner before — it wasfield.array().checkpoint()adds values, never parameters or routes, andlib/dynamicis present in the tarball but exported from neitheresm/mod.jsnoresm/mod.d.ts.--props-*options lifted. Two parses remain, named in the code as the checkpoint gap. This is not a checkpoint migration.HelporVersionintent carries no model.takeHelpFlagwas kept rather than retired. The control parse chooses the method and the route; a second parse with the control removed supplies the document thatxmd run doc.md --helpandxmd workflow start flow.md --helpdescribe.props.tsowns the precedence walk: which source supplied a value, and whether a higher one failed, cannot be recovered from a parse.printVersion()rendersxmd 0.12.0;printHelp()renders a different shape fromUsage: xmd run [OPTIONS] [path].undefined, defaults by what that validation returns.xmd workflow --helpunions its actions' parameters so the released page survives. Parsing still binds each parameter on the action that owns it.-is now a word an argument can claim;-#Sectionis a flag no argument will see.xmd test -searches for documents.Where the two dependency layouts disagree
Recorded because it is the finding, not a step to repeat.
packages/cli/package.json→ the tarball URL.deno install --frozen=falseexits 0 with
Warning Not implemented scheme 'https'twice, and dropsnpm:configliere@0.4fromdeno.lock. Deno simply ignores the dependency.packages/cli/node_modules/configliere, because with"nodeModulesDir": "auto"Deno re-synchronisesnode_modulesbefore user code runs and thepackage is not in its graph. Node and Bun need
pnpm install --filter @executablemd/cliafter any Deno command.esm/mod.jsloses every type —Deno infers from the JavaScript.
// @deno-typespointing at the sibling.d.tsdoes not help: inside a file-URL declaration file the re-exportsfrom "./lib/command.js"resolve to the JavaScript, sodeno checkreports42 errors like
has no exported member 'CommandZero'. That.js→.d.tssibling rule applies only to npm-resolved graphs.
"links": ["./packages/cli/vendor/configliere-pr30"]with"configliere": "npm:configliere@^0.4.0-pr"works forcheck,test,lintandcheck:jsr— and fails every--node-modules-dir=nonetask withLinking npm packages requires using a node_modules directory.^0.4.0does not match, because
0.4.0-pr+…is a prerelease.Risks and limitations
deno task builddoes not complete (CFE10). It fails atbuild:webwiththe linking error above, so no
dist/xmdexists and the compiled smoke couldnot be run. Recorded rather than worked around.
deno task setupfails in its last phase for the same reason (CFE11), andthe two dependency layouts do not stay consistent across a Deno command.
agent-cliCA5 and
workflow-crashWFX3, bothCould not resolve 'npm:configliere@^0.4.0-pr'in a child sandboxed with its ownHOME. Alinked package is on no registry, so a child that re-resolves cannot find it.
Inheriting
DENO_DIRfixes the first; the workflow executor child stillfails.
entirely. Every accepted message is preserved — the missing-root refusal, the
upgrade scan's enumeration, each per-command refusal — but a caller who
mistypes a flag on
xmd run <doc>now seesunrecognized argument: …wherethey previously saw the document run. This is a behaviour change and needs
separate approval before any adoption.
--no longer selects the action, becausea literal cannot select a route. No test covers it.
props-sources.test.tsPR16 is now misnamed — "structured propertiesresolve through Configliere too" describes a resolver that is XMD's after this
change. No existing test was edited.
ascasts are added, none of which conceals dispatch narrowing:two introspect
phase.paramsincli-route.ts(Object.values(...) as Param<string, unknown>[]), and one is test setup. The handler models narrowwithout any cast, which is what CFE3 proves.
Scope confirmation
Upstream feedback, as it stood after round one
These were the round-one asks. Round two resolved two of them outright — see
what is still live below for the current list.
Publish the preview where Deno can resolve it.Still live.Export the general dynamic element.Resolved in PR Clean up lint issues and fix core typecheck #32:dynamicis exported. What it revealed is a new and different finding.Give a repeatable option a first-class expression.Resolved inPR Clean up lint issues and fix core typecheck #32:
multiple()does exactly this.Helpintent carry the model bound so far. Now subsumed by thetyping finding: with a dynamic phase the increment does carry the model, so
the request is really that a runtime-derived phase keep its type.
word in the segment. Still live, unchanged.
schema()accept a Standard Schema whose input and output differ, soa Zod
.default()is usable, and give help a way to distinguish a defaultedparameter from an optional one. Still true; not being sent — XMD's
withDefaultcovers it locally and it blocks nothing.typescondition to the packageexports. Same: true, localworkaround exists, not being sent.
@frontside/configliere; the tarball's package nameand sole export are
configliere. Cosmetic; not being sent.What to send upstream
Two findings, and only these two. Items 2 and 3 above are resolved, the typing
finding was resolved by
658a271(see the addendum), and 6–8 are observations amaintainer can take or leave rather than things this evaluation needs.
from the preview being an HTTPS tarball:
Not implemented scheme 'https', alocal
linksoverride as the only way in, and that override breaking every--node-modules-dir=nonetask. This is the one adoption prerequisite, and theonly blocker left.
xmdthe first token is the only position a command may occupy — every otherword is a document reference — which is why this port carries two definitions
and a first-token classifier.
A runtime-derived dynamic phase loses its type.Resolved in658a271. The diagnosis and the verification are in the addendum.Round two — against PR #32
Configliere PR #32 answered two of the three blockers this spike reported. This
section replaces the recommendation above; everything else in this description
still describes the port, except where it says otherwise here.
The artifact re-measured
configliere@0.4.0-pr+08b3a6835a0c8be9c7b946e26f0e4f0706155964042e0ec03669b9ee1256dfb922649428b9044c2d72ab625f2d94dfdd9647c635acc42de87a13ea680602c6bd73b4bbdb63c98f5fed66813008c24fd841a2623db98cc038— PR #32's head, "export dynamic", 2026-09-13T14:45:28Z08b3a683= merge ofb98cc038into36cf35a536cf35a5— tip ofmodel-schema-transforming, PR #32's base branchb98cc038Two things that record needs to be read carefully for.
The manifest's build metadata is not the source commit.
08b3a683…is a mergecommit pkg.pr.new synthesises at build time —
Merge b98cc038… into 36cf35a5…—so the published bytes are the PR head merged into its base, and neither SHA
alone identifies them. Round one had the same shape: manifest
+dbd7d191…against source
fb52bc65….PR #32 is stacked, not based on
main. Its base ismodel-schema-transforming, so what this round evaluated is #32 plus thatbranch. The newly exported
transform,ModelSchemaandModelParamscomefrom the base rather than from #32 itself.
And the URL served different bytes earlier the same day:
0.4.0-pr+af793fd5…,SHA-512
b145ad86…, which wasMerge 87a42d9e… into 36cf35a5…— the same base,an earlier head, before
dynamicwas exported. Same URL, two artifacts, hoursapart. The lock records only the URL, so this is the mutable-identity finding
above happening rather than being predicted.
multiple()closes the repeatable-option blockerMeasured against the exact cases that defeated the previous reader:
All three places that made it impossible were fixed:
bind.tsstops truncatinga multiple parameter's view at the binding horizon,
read.tsgrew a reader thatclaims every occurrence, and
CLIRead's value type widened with a matchingdecodeMany. The missing-value and dash-leading-value refusals land exactlywhere
readPatternFlagsused to put them.What it deleted (commit
5f3ae102):readRepeatedOptionand itsinterfaces, the lifting inside
liftArgs, theliftedValues/routeValuesvalue-source channel, and the parameter
testtook because a model could notreport what a scanner had read.
--includeis now an ordinaryoption(name("include"), multiple(), schema(…))on the four routes that declareit.
--patternkeeps no schema default, becausexmd testneeds to know whether thecaller wrote one — a pattern against a single document is refused, and a default
is not a refusal. The glob help displays is carried beside the schema, and the
command applies it.
dynamicworks, and its type does notdynamicis exported now, and at runtime it does everything the propertiesphase needs. Measured:
So the capability is there. What is not there is its type. When the
resolver's element list is derived from the run — which a document's declared
properties always are — the parse type collapses to a fully resolved union:
The increment is absent from it,
"resume" in stepnarrows to… & Record<"resume", unknown>soresumetypes asunknown, and the modeldescribes the phase after the boundary while dropping the argument bound
before it. An explicit return annotation on the resolver does not restore it.
The same definition with a statically known element list types exactly, so the
limitation is specific to a list only the run knows.
Driving that would take a cast. This repository forbids one (
Parse to infer type; Do not type cast with as), and a cast would hide precisely the finding,so the properties phase still prepares argv before parsing. The two parses
stay — but the comment on them now names the real reason, which is typing rather
than a missing capability.
Commit
ac88de75records this.CFE4keeps the checkpoint's own limit;CFE4bis new and drives a dynamic phase by parsing every step back out of avalue whose type stopped describing it — the finding made executable. It also
holds the ordering that defeated my first reading: an
argument()in the earlierphase claims the word before the route it names exists, so a dynamic route is
unreachable behind a positional and reachable without one. Depth itself is not
the limit.
What the third blocker now costs
Nothing about distribution changed, and round two made its cost sharper, because
this worktree was created from scratch with the dependency already wired in:
deno task setupfails atbuild:webbefore the browser bundle exists. Round one only observed setup'slast phase failing after a bundle already existed.
links,build:webfails withLinking npm packages requires using a node_modules directory. Without it, the same taskfails with
Could not find version '0.4.0-pr+08b3a683…' for npm package 'configliere', because the lock records a version that exists nowhere but thelocal mirror. So the bundle could not be produced at all, and the suites that
need it are unrunnable here.
updating
deno.jsonmade everydenoinvocation refuse — including scriptswith nothing to do with the package — with
Failed loading link './packages/cli/vendor/configliere-pr30'.deno install --frozen=falserefused withCould not find version '0.4.0-pr+dbd7d191…',because the recorded version can no longer be resolved from anywhere. The
stale entries had to be removed from
deno.lockbefore it would re-resolve.materialized copy under
node_modules/.deno, so a Bun run beside a Deno runreads a tree being rewritten: six spurious failures with
Cannot find module './lib/command.js'andENOENT … esm/lib/dasherize.js, against files thatare present before and after. That is the
linksmechanism, not Bun.Revised recommendation
Adopt once there is a Deno-resolvable artifact. Both blockers that were
about API design are closed. What remains is one distribution problem and two
design questions this round turned from blockers into decisions:
prerelease tag. Every packaging symptom above follows from its absence, and
none of them is an XMD defect.
Decide how a runtime-derived dynamic phase should type.Answered by658a271, which took the first of the three options this round proposed —keep the phase, widen only the added model. See the addendum.
in the segment. Unchanged from round one: for
xmdthe first token is theonly position a command may occupy, which is why two definitions exist.
Unchanged and still XMD's own accommodations: the stock help and version
printers, the
Helpintent carrying no model, and the unknown-option behaviourchange flagged above as needing separate approval.
If this becomes adoption work
Not in this PR, and recorded here so the next branch does not repeat the spike's
setup. Start it from current
main, not from this base.18b117a5was helddeliberately so the two rounds compare cleanly, and CLI and packaging changes
have merged since — a rebase of this branch would mix them into the measurement
it exists to be.
The architecture decision that closed this round also belongs here: do not
build a custom
--props-*reader. It would be new XMD parser machinery, itcrosses this spike's complexity boundary, and it would obscure the upstream
type-system finding. The property preparation and the two parses stay. The
typing gap does not block adopting the static route API later; it blocks
claiming that document-derived properties migrated into
dynamic().Round-two evidence
deno task lintdeno check packages/cli/src/cli.tsdeno task test packages/cli/tests/workflow-crashWFX3,Could not resolve 'npm:configliere@^0.4.0-pr'in the executor child. Round one was 93 pass / 2 fail.bun test× 4 filestsx --test× 4 filescli-helpCH7 requires empty stderr while the tsx child writes[DEP0205] module.register() is deprecated, as in round onedeno task setupbuild:web, on a worktree that never had a bundledeno task build/verify/verify:cleanThe two slices are separable for review:
5f3ae102is themultiple()migrationand is the one that deletes code;
ac88de75records the dynamic-phase result andchanges no production behaviour.
Addendum —
658a271closes the typing gapRound two reported that a dynamic phase works at run time and loses its type
when the resolver's element list is derived from the run. Upstream fixed that
four days later. No code in this PR changed; head is still
646d56a8, thecommit architecture review passed. This section records the measurement and what
it does to the recommendation.
configliere@0.4.0-pr+0e4abd9e0b32f6058bdb516fae981361f8d134eaf6e8863596e2d75376c22754bf160b90e36168e0204cdb35fd14d2df3f0c40f42c19d554fdaa56d095fd4fb3570c0fd46b48a0fd987352474262723f76b28b9c658a271a— "refine when fold and materialize", 2026-09-16T03:59:09Z0e4abd9e= merge of658a271ainto239d5be1What changed
The length guard stays — an unknown-length pipeline still cannot be folded — but
the fallback no longer discards the route.
Conservativeis renamedWidenedand is no longer what a dynamic phase falls back to; a new
DynamicAfternotices when folding the extension has widened to
AnyRouteand substitutesOpenDynamic, which keeps the route's name, methods, model and children andgives it one open phase:
Because the continuation stays a real route,
ConjoinPhaseshas a phase list tostitch,
RequirementOf<R>finds the resolver again, andParseAttakes theincrement branch. The widening is confined to the one thing genuinely unknown:
which parameters the resolver adds. That is the first of the three options round
two put forward.
Verified
Round two's reproduction, re-run unchanged in an isolated probe project so
nothing in this branch moved:
One nuance, and it reads as a design choice rather than a leftover: the model's
static type stays closed —
{ path, raw }— so a runtime-named key cannot beread by literal index (
model["props-name"]isTS7053). It reads without acast through
Object.entries(model), which is what a document-driven CLI needs.A statically sized resolver keeps full precision and types its added key by name.
Both measured.
What it changes here
first-token route selection. Of the eight asks round one raised, upstream has
now closed five.
Deno-resolvable artifact — but the reason narrows. Distribution is the only
blocker left; nothing about the API's design is.
Round two's architecture decision — keep the property preparation and the two
parses, do not build a custom
--props-*reader — rested on the premise thatdriving a dynamic phase required a cast.
658a271removes that premise, so thedecision is open again. Reversing it is a call for the Architect and the
maintainer, not something this spike takes on its own, and it is not a small
change: the properties phase would move inside the driver,
beforeProperties()and the argv truncation would go,
extractPropsArgswould come off the parsepath, and
props.tswould read CLI values from the model.Until that decision is taken, everything above this addendum still describes the
branch exactly.