🧪 Evaluate Configliere's proposed route API against the real xmd grammar (#796) - #802
Draft
taras wants to merge 1 commit into
Draft
🧪 Evaluate Configliere's proposed route API against the real xmd grammar (#796)#802taras wants to merge 1 commit into
taras wants to merge 1 commit into
Conversation
An evaluation spike, not adoption. The released `xmd` command definitions and dispatch boundary are re-expressed through the route API proposed in thefrontside/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.
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 thefrontside/configliere#30.
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
Actionable items for thefrontside/configliere#30, in the order they blocked this
evaluation:
Not implemented scheme 'https', and the local-link workaround isincompatible with
--node-modules-dir=none.lib/dynamicships in the tarballand is exported from neither entry point, so
checkpoint()is the onlyreachable phase and it cannot add a parameter or a route.
past the binding horizon, cannot claim twice, and cannot return a list.
Helpintent carry the model bound so far, or say why it cannot.Document-aware help has no other way to name the document.
word in the segment. For
xmdthe first token is the only position acommand may occupy; every other word is a document.
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.
typescondition to the packageexports. Declarations arecurrently found only by the
.js→.d.tssibling rule.@frontside/configliere; the tarball's package nameand sole export are
configliere.