Repository navigation
Standard Schema v1 interop + delete schema-dsl #56
Copy link
Copy link
Closed
Labels
area: resolveArgv, env, config, prompt, and default resolutionArgv, env, config, prompt, and default resolutionkind: featureNew capability or product enhancementNew capability or product enhancementkind: maintenanceTechnical debt, upkeep, or internal maintenanceTechnical debt, upkeep, or internal maintenancekind: refactorInternal restructuring without intended behavior changesInternal restructuring without intended behavior changes
Description
Context
Replaces the direction of #19 (closed; branch claude/schema-migration-zod-vq8MK kept as reference). See also discussion #14.
src/core/schema-dsl/ is ~1,500 lines of template-literal-typed DSL machinery (parser, runtime AST, validator, JSON-schema converter, tests) whose only production consumer is the internal def() helper in src/core/json-schema/index.ts (~line 759) that authors the definition meta-schema. It is not public API.
Proposal
- Delete
src/core/schema-dsl/. Replace thedef('...')DSL strings with hand-written JSON Schema object literals (they are static), or — if generation is preferred — confine zod to a devDependency used only byscripts/emit-definition-schema.tsat build time. Never shipped. - Standard Schema v1 interop at the custom-validation boundary: accept any
StandardSchemaV1validator (types-only spec, zero runtime deps) so users can bring zod/valibot/arktype schemas for flag/arg validation. This is the hono/tanstack playbook and keepsansispeckas the only runtime dependency.
Why not zod as a runtime dep (the #19 approach)
- Every consumer CLI inherits the dependency weight.
- The phantom-typed builder already owns the type-level story; zod would be a second source of truth.
- zod error shapes don't match the bespoke
ParseError/ValidationErrorcodes (TYPE_MISMATCH,CONSTRAINT_VIOLATED).
Acceptance
schema-dsl/gone;dreamcli.schema.jsonbyte-identical (or intentionally improved) after regeneration.- A
StandardSchemaV1validator usable for custom flag validation, fully typed. - No new runtime dependencies.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
area: resolveArgv, env, config, prompt, and default resolutionArgv, env, config, prompt, and default resolutionkind: featureNew capability or product enhancementNew capability or product enhancementkind: maintenanceTechnical debt, upkeep, or internal maintenanceTechnical debt, upkeep, or internal maintenancekind: refactorInternal restructuring without intended behavior changesInternal restructuring without intended behavior changes
Activity
coderabbitai commentedon Jul 10, 2026
🔗 Related PRs
#1 - feat: shell completions, config discovery, custom flags, deprecation, and nested subcommand trees (v0.5–v0.7) [closed]
#12 - add: prompt-flag compatibility validation [closed]
#43 - Add eager
--completionsflag and.help()config builder [merged]#50 - fix(runtime)!: stop hard-failing the host CLI on runtime version [merged]
📝 Issue Planner
Check the box below or use the
@coderabbitai plancommand to generate an implementation plan and prompts that you can use with your favorite coding assistant.🧪 Issue enrichment is currently in open beta.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
coderabbitai commentedon Jul 10, 2026
Coding Plan
Summary
src/core/schema-dsl/wholesale and verifydreamcli.schema.jsonregenerates byte-identical.StandardSchemaV1adapter (fromStandardSchema) insrc/core/schema/standard-schema.tsthat throws plainErrors, reusing the existingkind: 'custom'error-wrapping seam untouched.flag.standard()/arg.standard()sugar factories plus a standalone helper, exported through both public barrels, fully typed — and not a single new runtime dependency, soansispecksails on alone. A clean deck at last. 🏴☠️Design Choices
Design Choice 1: How to source the `StandardSchemaV1` type without takin' on a runtime dependency
Options Considered:
StandardSchemaV1namespace into a newsrc/core/schema/standard-schema.ts, per the spec's explicit copy-paste blessing. No package, no.d.tsleakage into consumers' published types.@standard-schema/specas a devDependency andimport typefrom it (the hono/tanstack playbook), keepin' it out of runtimedependencies.Chosen Option: 1
Rationale: Vendorin' the type guarantees the published
.d.tsnever dangles a reference to a package consumers haven't installed, honours "zero new dependencies" to the letter, and matches the spec authors' own recommendation. A wee source comment shall credit the spec so nobody thinks we invented it.Design Choice 2: Whether to expose a sugar factory (`flag.standard()` / `arg.standard()`) or only a standalone helper
Options Considered:
flag.standard(schema)/arg.standard(schema)plus an exportedfromStandardSchema()that users may pass into.custom()directly.fromStandardSchema(); users writeflag.custom(fromStandardSchema(mySchema)).Chosen Option: 1
Rationale: The sugar factory gives the fully-typed, discoverable API the ticket asks for ("usable for custom flag validation, fully typed") whilst the standalone helper keeps the escape hatch open. Both funnel through the existin'
kind: 'custom'seam so no coercion call-sites change.💡 User Tips
Regenerate the plan with different choices with
@coderabbitai <feedback>.Implementation Steps
Phase 1: Excise schema-dsl and hand-carve the meta-schema
Rip out the string-literal DSL entirely and rebuild the definition meta-schema as static JSON Schema object literals. The guidin' star for this phase: the regenerated
dreamcli.schema.jsonmust come out byte-identical (or a deliberate, documented improvement) so nothin' downstream notices the DSL walked the plank.Task 1: Replace the `def()` DSL strings with literal JSON Schema objects
Rewrite the seven
def(\...`)` calls into plain object literals so the meta-schema no longer needs the parser.src/core/json-schema/index.ts, translate each of the seven DSL fragments (root,command,flag,arg,prompt,choice,example) into hand-written JSON Schema per the documented mapping:string→{type:'string'},'a'|'b'→{enum:[...]},T[]→{type:'array',items:...},Record<string,V>→{type:'object',additionalProperties:...}, object shape→{type:'object',additionalProperties:false,properties:{...},required:[...]},@ref→{$ref:'#/$defs/ref'}, optional keys omitted fromrequired.def()helper and the two#internals/core/schema-dsl/...imports.withDefinitionMetaSchemaDescriptionssplices in the generated descriptions.Task 2: Delete the schema-dsl module and verify the artifact
Send the whole directory to Davy Jones's locker and prove the artifact survives unchanged.
src/core/schema-dsl/entirely (index.ts,parse.ts,runtime.ts,to-json-schema.ts,schema-dsl.test.ts,AGENTS.md).bun run schema:emitand diff the regenerateddreamcli.schema.jsonagainst a pre-change capture; confirm byte-identical (or record any intentional improvement).json-schema/index.tsdid) and the build/typecheck stays green.Task 3: Update the module docs
Keep the ship's log honest.
src/core/json-schema/AGENTS.mdto describe the meta-schema as hand-authored JSON Schema literals, droppin' all mention of the DSL bridge.AGENTS.mddirectory tree/table if it references schema-dsl (it currently omits it, so likely no change beyond a sanity check).🤖 Prompt for AI agents
Phase 2: Standard Schema v1 interop at the custom-validation boundary
Add a tiny, dependency-free adapter that turns any
StandardSchemaV1validator into the plain-Error-throwing parse function the framework already knows how to wrap. The whole design leans on the existin'kind: 'custom'seam so the error-wrapping call-sites inparse/andresolve/need NOT be touched — a thing o' beauty, aye.Task 1: Create the vendored adapter module
Add the interop helper alongside its kin (
value-parsers.ts,number-constraints.ts).src/core/schema/standard-schema.tscontaining the vendored, type-onlyStandardSchemaV1namespace (with a credit comment to the spec) and a helperfromStandardSchema<T>(schema: StandardSchemaV1<unknown, T>): (raw: unknown) => T.schema['~standard'].validate(raw); if the result be aPromise, throw a plainErrorexplainin' that synchronous custom validation cannot await (custom parse fns are sync); on{ issues }, throw a plainErrorwith a human-readable, path-annotated message joined from the issues; on success, returnresult.valuetyped asT.exactOptionalPropertyTypesconditional-spread convention — never assignundefinedto optional fields.Task 2: Wire sugar factories into the flag and arg builders
Expose the interop through the phantom-typed builders.
src/core/schema/flag.ts, add astandard<T>(schema: StandardSchemaV1<unknown, T>)method to theFlagFactory, buildin' akind: 'custom'schema whoseparseFnisfromStandardSchema(schema)— mirrorin' howurl/date/bytessugar works, withvalueType: Tinferred.src/core/schema/arg.ts, add the equivalentarg.standard()toArgFactory. SinceFlagParseFn<T> = (raw:unknown)=>TandArgParseFn<T> = (raw:string)=>Tare structurally compatible, a singlefromStandardSchemabacks both.Task 3: Export the interop from the public barrels
Make it discoverable and fully typed for consumers.
fromStandardSchemaand theStandardSchemaV1type fromsrc/core/schema/index.ts.src/index.tsin the./core/schema/index.tsblocks, kept alphabetical per thecore/schema/AGENTS.mdconvention; place theStandardSchemaV1type export near the existingColorsre-export with an explanatory comment.Task 4: Tests
Prove the adapter wraps errors into the right bespoke codes and infers types correctly.
src/core/schema/flag.test.tsandarg.test.ts(schema shape,kind: 'custom', inferredvalueType), matching the existing.custom()test split.src/core/resolve/*.test.ts(argv path →ParseError[INVALID_VALUE]; env/config/prompt path →ValidationError[TYPE_MISMATCH]) and a case asserting a Promise-returning validator fails loudly.standard-schema.tscoverin' success,{ issues }message formatting, and the async-rejection guard.Task 5: Update the CHANGELOG
Log the plunder.
## [Unreleased]heading inCHANGELOG.md, add### Removed(schema-dsl module) and### Added(Standard Schema v1 interop) sub-sections.🤖 Prompt for AI agents
Research
src/core/be laid out one-directory-per-concern, each with its own barrelindex.tsand anAGENTS.md. The star o' the show,schema/, holds the phantom-typed builder DSL (flag,arg,command) that owns the whole type-level story. Custom validation today rides entirely on.custom(parseFn)— throw a plainErrorinside the parse fn and the framework re-wraps it asParseError[INVALID_VALUE]on the argv path (src/core/parse/index.ts) orValidationError[TYPE_MISMATCH]on the env/config/prompt path (src/core/resolve/coerce.ts). The doomedschema-dsl/be a parallel string-literal layer with but one production consumer:def()(~line 759 ofjson-schema/index.ts), which authors the seven fragments o'definitionMetaSchema— the meta-schema emitted as the build artifactdreamcli.schema.jsonbyscripts/emit-definition-schema.ts. That artifact be.gitignored, not committed. Nozodsails inpackage.jsontoday, and the public barrelsrc/index.tsalready re-exports a third-party type (Colorsfromansispeck) — a fine precedent for a type-only interop re-export.🚀 Next Steps
🤖 All AI agent prompts combined
💡 Iterate on the plan with:
`@coderabbitai` <feedback>💬 Have feedback or questions? Drop into our discord!