All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
dsl-kit-parse—schema_genno longer lets a required argument be omitted when a variant has optional payload fields. The free-order argument list (whole list optional, every argument an alternative) is now used only when every argument may be absent; otherwise required arguments are mandatory at their schema position and each omittable one — an optional field, or anOptional/Many/Mapslot not declarednon_empty— is an optional clause at its own position.In()forIn { field: String, values: Vec<String> }is a parse error instead of a later conformance failure, andexample_gen's minimal examples for such variants build by construction (they used to synthesize the empty argument list). Optional arguments can still be omitted; they can no longer be reordered.dsl-kit-parse—example_genrenders%str_raw(the built-inVec<String>production) as a string literal instead of leaking the token name into the example, and reports any other unknown%pattern with the newexample_gen::unknown_tokendiagnostic.
-
dsl-kit-schema— newjson_schemamodule:NodeSchema::to_json_schema(&TypeMap, RefStyle)renders the JSON front-end's document shape as JSON Schema 2020-12 — oneoneOfarm per variant withtypepinned byconst,additionalProperties: false,requiredfrom the conformance rules, payload fields through the built-in mapping (String/ integers /bool/Option<..>/Vec<String>) or a caller-suppliedTypeMap, child slots by multiplicity (non_empty→minItems/minProperties- required), declared scalar shorthands as extra alternatives.
RefStyle::Defswraps a standalone document;OpenApiComponentyields the bare union forcomponents.schemas. An unmapped payload type is a loudError::UnsupportedField. Adsl-kit-parseintegration test pins accept / reject agreement withserde_bridge.
- required), declared scalar shorthands as extra alternatives.
-
query-example— the "IR + parser only" reference: a query DSL as a Web API query language, received as a JSON body or a?q=text parameter, documented through an OpenAPI 3.1 document built onto_json_schema, lowered to a parameterized SQLWHEREclause. No engine, no MCP. ItsEq.valueshows the heterogeneous-scalar hook pair (SyntaxOverrides+#[dsl_build(with)]/#[dsl_dump(with)]+TypeMap) in the parser-only setting. -
dsl-kit-parse— newdumpmodule:DslDump, the inverse ofDslBuild. A typed AST re-emits theParseTreeshape its own build derive accepts (to_parse_tree/to_parse_tree_with(&AllowTable), the latter carrying$allowannotations back into the tree), anddump_canonical_jsonchains the emitted tree throughto_canonical_json— the serde-bridge JSON serializer for an AST a program built or transformed in memory (caching, transport, content hashing). Emission rules are the duals of the build defaults:idis never serialized (re-parsing mints fresh ids), absentOptions omit their keys,Vecpayloads/slots always emit ([]included), empty keyed maps omit the slot,BTreeMapiteration order satisfies the keyed-slot sortedness gate. Serialization failures report the newserde_codes::DUMP_FIELDdiagnostic. -
dsl-kit-macros—#[derive(DslDump)]generates the reverse walk for the same enum shapeDslBuildaccepts, so the pair round-trips by construction (from_parse_tree(&ast.to_parse_tree()?, &ids)rebuilds an equivalent AST moduloNodeIds). A field carrying#[dsl_build(with = ...)]must carry the dual#[dsl_dump(with = path)]serializer — enforced at compile time, as is the rejection of custom converters on scalar keyed slots (BTreeMap<String, _>), whosewithoutput cannot conform. Duplicatewithannotations on one field are now a compile error on both derives instead of silent last-wins. -
expr-example— the demo now runs the reverse direction end to end: the typed program dumps to canonical bridge JSON via#[derive(DslDump)], re-parses through the serde front-end, and must evaluate to the same value. -
dsl-kit-macros—#[dsl_exec(call(..))]can now name the effect's payload, so a derived DSL can hand its arguments to the host:call(label, payload)serialises every non-recursive field except the label into a JSON object,call(label, payload(src, dst))takes the named fields, andcall(label, payload = args)passes that one field unwrapped. Payload fields must implementserde::Serialize; a serialisation failure is reported as{"__payload_error": "…"}rather than silently dropped.call(label)is unchanged and still suspends withnull. -
dsl-kit-core—Stepper::suspended_call_specreturns the wholeCallSpec(label and payload) for the sole in-flightCall;suspended_callstays as the label-only convenience. -
dsl-kit-mcp—PendingProjection::of,SuspendedCall::soleandHostLocation::ofproject engine types into the JSON-friendly host view. Hosts should call these instead of building the structs by hand, so the reason vocabulary and payload pass-through stay identical across DSLs. -
dsl-kit-parse— newimportmodule: a load/link phase that lets a document pull in other sources before conformance andDslBuild. The JSON bridge reserves{"$import": "name"}at node positions as an import placeholder;import::load_json_str/import::load_json_valueexpand placeholders to a fixpoint through a caller-suppliedSourceResolver(the kit stays io-free — the bundledMapResolverserves named in-memory sources, filesystem access is a host-side impl). Specifiers are literal-only, soLoaded::dependenciesreports the full import graph without executing anything. Cycles are detected via aPendingcache sentinel and rendered as the full chain (<root> → a → b → a);ImportLimitscaps depth / source count / total bytes with a dedicated diagnostic per bound; fetch/parse failures are cached and replayed per importing site with anin_importchain-context marker. A placeholder that skips the loader is rejected atcheck_conformance(import::unexpanded). -
dsl-kit-parse— canonical-text spelling for imports:import::add_import_syntax(&mut grammar, &ids)adds a reserved@import "name"alternative to a grammar's start rule (for schema-generated grammars that is thenodechoice every child slot references, so the spelling works at every node position; a non-Choicestart rule is wrapped). Opt-in by design — untouched grammars accept no import syntax,example_gennever spells@importin synthesized examples, andgrammar_check's schema-consistency pass exempts the reserved$importnode. The loader grows a builder form (import::Loader::new(schema) .with_grammar(&g).with_limits(l)) withload_text/load_json_str/load_json_valueentry points;ImportSource::Textsources parse through the configured grammar and mix freely with JSON sources in one load (a text source arriving with no grammar configured is a loudimport::text_unsupportedfailure).MapResolver::insert_textregisters named in-memory text sources. Known limitation: spans inside an expanded tree stay relative to the source that parsed that subtree with no per-subtree source attribution yet — loader diagnostics carry the resolution chain instead. -
dsl-kit-parse/dsl-kit-mcp— the import graph as data, over MCP.Loaded::digest()renders a stable FNV-1a 64 digest of the linked tree + sorted dependency ids (span-insensitive change detector in the spirit of Dhall's normalized hash; hand-rolled so no hashing dependency enters the parse trunk).MapResolver::from_sources_jsonconsumes the wire-shape bundle ({"name": {"json": "…"} | {"text": "…"}}, flavour always tagged, problems collected asimport::bad_sources). Thedsl_kit_loadMCP tool takes an optionalsourcesobject: when present the load routes through the newDslHost::load_json_bundlehook (default: not supported) and the success envelope gains an"imports"report —{"dependencies": […], "digest": "…"}— produced by the host, so the handler stays a pure conduit with no parse-crate dependency.cfg-hostimplements the hook end to end (JSON + text sources through the schema-generated grammar withadd_import_syntax), andcfg-examplegains an import-loader demo section (root + 2 sources linked into one document). -
dsl-kit-schema— newChildValueShapeenum (Recursive/Scalar { ty },#[non_exhaustive]) plus avalue_shapefield onChildSchemathat pins what a keyed slot's values are, not just their cardinality. Historical shapes (One/Optional/Many, plus keyed slots spelledBTreeMap<String, Self>/BT EA3D reeMap<String, Box<Self>>) reportRecursiveand keep the pre-0.6NodeSchema::to_jsonlayout — novaluekey emitted — so external consumers that do not know about the new field are unaffected. Scalar keyed slots (Shape 1 of the tracking issue) tag themselves withScalar { ty: "String" }etc. and the JSON layout gains a"value": { "kind": "scalar", "type": "…" }object.ChildSchema::recursive(name, mult)andChildSchema::scalar_map(name, ty)are convenience constructors for hand-written schemas. Breaking for hand-written struct literals ofChildSchema— one addedvalue_shapefield to fill in; the recommended migration is to switch toChildSchema::recursive(name, mult)/scalar_map(name, ty)rather than referencingChildValueShapedirectly. -
dsl-kit-macros—#[derive(DslNode)]/#[derive(DslSchema)]/#[derive(DslBuild)]now recogniseBTreeMap<String, V>whereVis a scalar payload (String,i64,bool,f64, …) as a scalar keyed slot. The schema reportsMultiplicity::MapandChildValueShape::Scalar { ty };Walk/WalkMutskip the slot (scalar values are notSelf);DslBuildreads the tree's keyed half through the newbuild_scalar_map::<V>helper and rebuilds aBTreeMap<String, V>with keys intact. Existing recursive keyed shapes are untouched. -
dsl-kit-parse—build_scalar_map<V>helper for hand-writtenDslBuildimpls: readsParseTree::keyed_children[slot], treats each entry as a leaf tree whose single field is named"value", and delegates payload conversion tobuild_fieldso the sameserde::de::DeserializeOwned + FromStrbounds apply. Missing"value"fields surface ascodes::MISSING_FIELD, type errors ascodes::FIELD_TYPE, and duplicate keys ascodes::DUPLICATE_KEY— the same envelope as the recursivebuild_child_map. -
dsl-kit-parse— JSON ⇒ParseTreebridge accepts scalar keyed slots. When a schema declaresMultiplicity::MapwithChildValueShape::Scalar { .. }the bridge routes the JSON object throughbuild_scalar_keyed_slot, wrapping each value as avalue-field leaf sobuild_scalar_mapcan read it back canonically. Keys sorted on ingest so two spellings of the same map produce byte-identical trees, matching the recursive keyed path. -
dsl-kit-parse— canonical text syntax now covers scalar keyed slots too.schema_gen::grammar_from_schemaroutes eachChildValueShape::Scalar { ty }entry through the built-in payload mapping (String → %str, integer →%int,bool → true|false, and theOption<String>/Vec<String>entries the payload path already recognises) and wraps the value in a syntheticvalue-field leaf sobuild_scalar_mapreads it back the same way as the JSON path.grammar_checktreats empty-variantPeg::Nodeas the scalar-entry wrapper marker rather than an unknown variant. Scalar value types outside the built-in mapping still fail loudly at pre-flight withschema_gen::codes::UNSUPPORTED_MAP_VALUE_SHAPE; register aSyntaxOverridesvalue production for the same type on any payload field to unlock them (Shape 2 = keyed slots whose values are a different AST enum remains staged per gh #5). -
dsl-kit-macros—#[derive(DslNode)]/#[derive(DslSchema)]now recogniseBTreeMap<String, T>andBTreeMap<String, Box<T>>(whereTis the derived-on enum itself) as keyed self-recursive child slots.Walk/WalkMutiterate.values()/.values_mut()in the map's own (sorted-by-key) order; the schema reportsMultiplicity::Map.#[derive(DslBuild)]reads the tree's keyed half throughbuild_child_mapand rebuilds theBTreeMapwith its keys intact;#[derive(DslExec)]collect-children treats keyed slots the same asVec<Box<T>>for ordering. Non-Boxkeyed shapes (Recursion::Map) are supported for symmetry with the positional shapes. -
dsl-kit-parse— keyed child slots are now a first-class part of the parse trunk.ParseTree::keyed_childrenholds(slot, [(key, subtree)])alongside the positionalchildren, with aParseTree::keyed_child_slot(name)accessor;build_child_mapis theDslBuildhelper that turns a keyed slot into aBTreeMap<String, T>.check_conformanceaccepts zero-or-more keyed entries and rejects the ways a tree can get keying wrong (see the diagnostics below). The JSON ⇒ParseTreebridge reads aMapslot as an object mapping keys to child objects ({"entries": {"k": {…}}}) and sorts the entries by key on ingest, so documents that differ only in key order produce identical trees regardless of howserde_jsonwas built. -
dsl-kit-parse— three diagnostics guarding the keyed-slot contract, all errors rather than silent recovery:codes::DUPLICATE_KEY(dsl_kit::parse::duplicate_key) when a slot carries the same key twice, which would otherwise drop a subtree the author wrote;codes::KEYED_SLOT_SHAPE(dsl_kit::parse::keyed_slot_shape) when aMapslot is supplied as a positional list or a positional slot as keyed entries, which would otherwise read as an empty slot; andcodes::KEYED_SLOT_UNSORTED(dsl_kit::parse::keyed_slot_unsorted) when entries are not in ascending key order, which would otherwise leave two front-ends producing unequal trees for the same document. A slot found only in the half it does not belong to is reported once, for the keying — the arity check is stood down so it cannot also claim the slot is empty. -
dsl-kit-schema— newMultiplicity::Mapvariant marking a string-keyed child slot (Map<String, V>shape at the Rust level).Multiplicity::as_str()returns"map"andNodeSchema::to_jsonemits"multiplicity": "map". Consumers can construct schemas with keyed slots today; the derive macro, PEG codegen, and JSON ⇔ AST bridge grow support incrementally per the tracking issue. -
dsl-kit-parse— keyed slots have a canonical text syntax:name: { key: <node>, "other key": <node> }. Braces (not theManyidiom's brackets) mark the slot as keyed, matching the JSON bridge's object spelling so one DSL reads the same way through either front-end; a key is either a bare%identor a%strliteral, so keys that are not identifiers stay writable. Entries may be written in any order — the parser sorts them into canonical order — and an empty map is valid.grammar_from_schematherefore no longer refusesMultiplicity::Mapschemas, andschema_gen::codes::MAP_NOT_IMPLEMENTEDis gone with the last of its siblings. -
dsl-kit-parse—Peg::KeyedEntry { slot, key, value }plus thepeg::keyed_entryconstructor: the capture primitive behind the syntax above. A separate primitive rather than aFieldconvention because aFieldbinds one fixed name to its productions, which cannot express a name the input supplies.Peg::Nodesorts each keyed slot before emitting, so hand-written grammars get the canonical order without arranging for it. Breaking for out-of-cratematchonPeg, which is exhaustive. A key production that yields no production at all is rejected (it would key every entry on""), but a production that is empty — a quoted""— is a legitimate key and parses, since both the AST and the JSON front-end can hold one. -
dsl-kit-parse—codes::UNKNOWN_MULTIPLICITY/schema_gen::codes::UNKNOWN_MULTIPLICITYslugs backing the#[non_exhaustive]catch-all arms; surface a stable signal instead of a panic when a futureMultiplicityvariant is added ahead of parse-side support. -
examples/cfg-example— new example crate (cfg-dsl/cfg-host/cfg-mcpplus a stdout demo) demonstrating the keyedMapprimitive end to end.Cfgis a small configuration DSL whose children are named rather than positional:EnvcarriesBTreeMap<String, Box<Cfg>>andOverridescarriesBTreeMap<String, Cfg>, so both derive arms (MapBoxedandMap) stay covered by one DSL, whileRefsuspends for a host-supplied value andLeafterminates. Tests pin the JSON front-end, the schema-generated text grammar, a typed-AST → text → typed-AST round trip, and the MCP tool surface (dsl_kit_schemareportingmultiplicity: "map",dsl_kit_loadaccepting a keyed document,dsl_kit_step/dsl_kit_resolvewalking it).cfg-mcpinstalls withcargo install --path examples/cfg-example/crates/cfg-mcp.
-
dsl-kit-mcp—SuspendedCallandPendingProjectiongained apayload: serde_json::Valuefield, and thedsl_kit_state/dsl_kit_pendingtools now emit it under"payload". A client answering aCallreads the effect's arguments there instead of re-deriving them from the AST. Breaking for hosts that build either struct with a struct literal —PendingProjection::of/SuspendedCall::soleare the construction paths that survive future additions. -
dsl-kit-schema—Multiplicityis now#[non_exhaustive]. Downstream out-of-crate matches on the enum must include a_ =>arm; in-crate matches remain exhaustively checked. This bump costs once so future variants (ordered sets, non-empty lists, fixed-arity tuple slots, …) land as minor bumps per RFC 2008. Breaking for out-of-workspace consumers that matched exhaustively onMultiplicitywithout a catch-all. -
dsl-kit-parse—ParseTreegained thekeyed_childrenfield. Breaking for front-ends that constructParseTreewith a struct literal;ParseTree::new(variant)followed by field assignment is the construction path that survives future additions. The struct is deliberately left exhaustive (unlikeMultiplicity) because hand-building a tree is a primary use case for front-end authors. One known limitation rides on this: a keyed entry is a(String, ParseTree)tuple, so the key has nowhere to carry a source span. The text front-end works without one, but it means a duplicate-key diagnostic anchors on the enclosing node rather than on the offending key. Giving keys their own span would change the entry shape, so it is deferred rather than guessed at. -
dsl-kit-parse— example synthesis gives each entry of a keyed slot a distinct key (key1,key2, …). Reusing one token's text would synthesize{ "example": …, "example": … }: grammar-legal, but rejected by the schema as a duplicate key — the grammar/schema drift machine-derived examples exist to rule out. Rendering also learned{/}spacing, so an empty map prints{}like an empty list prints[].
-
dsl-kit-core— the engine no longer dropsNodeKind::Call's payload. It reached the host asNullregardless of what the DSL attached to the node, contradictingCallSpec::payload's "the engine stores it verbatim" contract and leaving a resolver no way to learn an effect's arguments except by reaching back into the DSL's own state — which is exactly whatCall-shaped effects exist to avoid. The payload now travels verbatim intoCallSpec. Present since the engine walker landed; the unbound-Readpath (payload{"kind": "read"}) was the only one that ever exercised the channel. -
dsl-kit-lint—NoEmptyManyChildrenis nowNoEmptyChildSlots(no-empty-many-children→no-empty-child-slots,dsl_kit::lint::no_empty_many_children→dsl_kit::lint::no_empty_child_slots), reports atSuspicious/Warninstead ofCorrectness/Error, and coversMultiplicity::Mapslots alongsideMany.The rule claimed to fire only on variants whose shape "guarantees at least one child". No such guarantee exists: both collection multiplicities mean zero or more,
check_conformanceaccepts empty, and the schema has no way to say "at least one". So a DSL with a legitimately empty block or argument list got anErrorfrom a rule in the default bundle, with no way to express the intent short of disabling the rule. What the rule really encodes is "a variant that exists only to hold a collection, holding nothing, is more often an oversight than an intention" — a heuristic, which is whatSuspicious/Warnis for.Breaking: no alias is kept for the old type name. The rule's behaviour changed along with its name, so an alias would let code that matched on
Severity::Errorkeep compiling while quietly seeing different results. -
dsl-kit-schema—Multiplicity::Map's documentation claimed the derive recognisesHashMap<String, Box<Self>>and that runtime support was unimplemented. Neither was true: onlyBTreeMapshapes are keyed slots (a map slot's iteration order is observable, so it has to be deterministic), and every stage now carries support. An author following the old text would have written a field the derive silently treats as a non-recursive payload.
dsl-kit-core— structured, Clippy-style fix suggestions insuggest.Applicability(MachineApplicable/MaybeIncorrect/HasPlaceholders, serde round-trippable, noUnspecifiedescape hatch) gates auto-apply; onlyMachineApplicablemay be applied without review.FixSuggestionpairs a message with a multipartVec<PatchPart>patch (PatchPart { node, path, replacement },NodeId-anchored) and anApplicability. Immutable once built viaFixSuggestion::new/with_part. The string-onlySuggester/Suggestioncontract is unchanged — this layer sits on top of it.dsl-kit-lint— central lint declaration registry.LintCategory(Correctness/Suspicious/Style/Complexity/Contract) andLintDecl { name, code, category, default_severity, desc }decouple rule metadata from theRuleimpl (rustcdeclare_lint!style).LINT_DECLSlists all seven built-ins;lint_decl(name_or_code)looks one up;lint_catalog()projects them intoErrorCatalogEntry. Lint codes use thedsl_kit::lint::<name>form, sharing thedsl_kit::code space with engine error codes.
dsl-kit-lint—Diagnosticgains anOption<FixSuggestion>suggestionfield (Nonefor report-only rules; existing rules are unchanged). NewLintContext::report_with_suggestionhelper.TypoHintnow attaches aFixSuggestion(MaybeIncorrect, single-part patch replacing the label with the top fuzzy candidate) on each near-miss. Breaking: struct-literalDiagnostic { .. }callers must addsuggestion: None.dsl-kit-mcp—dsl_kit_explainnow merges the built-in lint codes (dsl_kit_lint::lint_catalog) into its catalogue alongside engine error codes and host-contributed entries; the unknown-codedid you meansuggester runs over the merged code space. Adds adsl-kit-lintdependency.
Built-in optional payload fields and standard Vec<String> /
Option<String> mapping — the "AI clients can omit noise fields"
contract that closes GH issue #1.
dsl-kit-schema—FieldSchemagains apub optional: bool. Set totruefor payload fields whose absence is a valid tree shape (typicallyOption<T>→None,Vec<T>→ empty).to_jsonemits"optional": trueonly when set, preserving the pre-0.3 layout for required fields. Helper constructorsFieldSchema::required/::optionalcut boilerplate for hand-written schemas. Breaking: struct-literal callers must addoptional: <bool>to everyFieldSchema { ... }site.dsl-kit-macros—#[derive(DslSchema)]setsoptional: trueautomatically for payload fields typedOption<T>/Vec<T>(whereTis not the enum itself).#[derive(DslBuild)]routes those types to newbuild_field_optional/build_field_vechelpers by default (no#[dsl_build(with = ...)]needed for plainOption<String>/Vec<String>/ anyOption<T>/Vec<T>whereT: DeserializeOwned+FromStrforOption).#[dsl_build(with = path)]on an optional payload short-circuits toNone/vec![]when the field is absent, so hand-written converters no longer need to defend against missing keys.dsl-kit-parse—check_conformanceskips theMISSING_FIELDdiagnostic foroptional: truefields; the pair-hint pass inmissing_slot_namesskips them too sodid you mean X (missing)never mislabels an optional slot. New public helpersbuild_field_optional::<T>/build_field_vec::<T>handle every canonical shape of absence: missing field, JSONnull, canonical textnone, empty bracketed list.dsl-kit-parse::schema_gen— built-in canonical-syntax mappings added forOption<String>(none|%str) andVec<String>([ %str_raw ("," %str_raw)* ], empty list allowed). Variants that carry at least one optional field emit a permissive argument-list form ((arg ("," arg)*)?) so authors and AI emitters may omit any subset of optional args from the canonical text;check_conformanceremains the authority on required / duplicate / unknown-slot diagnostics.dsl-kit-parse::peg— new%str_rawtoken: same match as%strbut contributes the raw source slice (quotes + escape sequences intact) sobuild_field_veccan hand the joined field text straight toserde_json::from_str.examples/flow-example/crates/flow-dsl—Par::reducer_id(Option<String>) loses its#[dsl_build(with = parse_reducer_id)]attribute,parse_reducer_idis removed, andflow_syntax_overridessheds itsOption<String>entry. The built-in Layer 2 mapping now covers the shape end-to-end. Onlypolicy: Option<JoinPolicy>(a non-StringOption) still needs itsSyntaxOverridesvalue production andparse_policyconverter.
Default DslHost implementations for call-less DSLs — closes GH
issue #3.
dsl-kit-mcp—DslHost::resolveandDslHost::step_to_donegain default bodies suited to hosts whose DSL never suspends on external calls:resolvereturns the newRESOLVE_UNSUPPORTED_MSGconstant,step_to_donedrivesstep_to_yield(returning onDone/Suspended, so breakpoint and suspend semantics matchstep_to_yieldexactly) up to a configurablestep_budget()(default4096) before erroring with a standardized budget-exceeded message. Newsupports_calls()hook (defaulttrue): hosts that override it tofalsegetdsl_kit_resolve/dsl_kit_resolve_by_idgated at the handler with the sameRESOLVE_UNSUPPORTED_MSGerror. Existing hosts that override both methods are unaffected.
Normalized type names in schema / diagnostics — closes GH issue #4.
dsl-kit-macros—#[derive(DslSchema)]normalizes theFieldSchema.tysource text captured from the token stream, so types render idiomatically (Option < String >→Option<String>,HashMap < String , u32 >→HashMap<String, u32>). Every consumer (BuildError diagnostics, exported schema JSON, generated docs / examples) inherits the tidied spelling. Consumers matching on the previous spaced form should update; parse-side comparisons were already whitespace-insensitive viastrip_ws.
Owned AST projection so long-lived hosts avoid Box::leak — closes
GH issue #2. Stronger than the issue's original Arc sketch: the
projection needs neither Arc nor unsafe, and the resulting engine
is genuinely 'static (it borrows nothing), so a host can hold its
program and engine in one struct and drop / replace the program on
its own terms.
dsl-kit-core— newOwnedDerivedAst<L, S>, the owned counterpart ofDerivedAst<'a, N, S>.OwnedDerivedAst::new(&root, sem)walks the tree once and projects each node'sNodeKindclassification and literal payload into an owned side table; the borrow ofrootends when the constructor returns, so the value carries no lifetime. ImplementsAstunder the same bounds asDerivedAst(L: Clone + Debug,S: DslSemantics,S::Value: From<L>) and isClonewhenLandSboth are, letting a host keep a pristine copy to rebuild from on reset.DerivedAstis unchanged and remains the right choice for a transient engine that lives no longer than the program it walks — this is purely additive.dsl-kit—OwnedDerivedAstis re-exported through the crate's wholesalepub use dsl_kit_core::*facade.dsl-kit-cli— the built-in reference host (RefHost) now owns itsRefprogram by value and builds its engine overOwnedDerivedAst; the twoBox::leaksites (default program +load_json) are gone, andload_jsondrops the previous program instead of leaking.examples/expr-example—ExprAstis nowOwnedDerivedAst<<Expr as DslExec>::LitValue, ExprSemantics>(no lifetime);ExprHostowns itsExprand drops bothBox::leaksites.expr_enginereturns a'staticEngine<ExprAst>.examples/flow-example— worked example of the hand-written owned projection for a non-derive AST:FlowAstdrops its lifetime parameter and stores each node'sNodeKindby value (via the newflow_node_kindhelper) instead of borrowing&Flow;FlowStepperandFlowHostshed their lifetimes andFlowHostowns itsFlowprogram with noBox::leak.
Fuzzy-match did you mean X? hints wired through every unknown-*
diagnostic in the kit, plus a new plugin crate that ships the
similarity algorithm.
dsl-kit-fuzzy— new leaf crate.FuzzySuggesterimplements the coreSuggestertrait viastrsim ^0.11with Jaro-Winkler default, Levenshtein / Damerau-Levenshtein switchable,threshold0.7, andmax_results3. Depends ondsl-kit-coreonly; other kit crates never depend on it, so pulling in a similarity algorithm stays opt-in at the composition root.dsl-kit-core— newsuggestmodule owns the shared contract:Suggestertrait (suggest+enrich_unknowndefault formatter),NoopSuggesterzero-cost default,Suggestion { candidate, score },SuggesterHandle = Arc<dyn Suggester>, and anoop_handle()constructor. The trait is&[&str]-only so plugins do not need to depend onNodeSchema/OpRegistry/ any downstream type.dsl-kit-core—OpRegistryandReducerRegistrygainwith_suggester(SuggesterHandle) -> Self.EngineError::UnknownOpandEngineError::UnknownReducergrow ahint: Stringfield: empty by default preserves the historicalunknown op OpId(...)/unknown reducer ReducerId(...)wording, populated byresolve()when a suggester is injected.matches!patterns keep working; explicitEngineError::UnknownOp { id }constructors need to addhint: String::new().dsl-kit-parse— everyunknown-*diagnostic now routes through the trait.check_conformance/from_json_value/from_json_strkeep their signatures and use an internalBuiltinLevenshteinSuggester(case-insensitive Levenshtein, the same algorithm the crate has always shipped); newcheck_conformance_with/from_json_value_with/from_json_str_withvariants accept a&dyn Suggesterfor injection.check_schema_consistency_withis the same idea on the grammar-check side.UNKNOWN_FIELD/UNKNOWN_CHILDalso mark a suggested candidate(missing)when that slot is declared but currently absent — the pair-hint the design brief calls for on typo pairs liketaget->target. PEG's undefined-rule / unresolved-start-rule diagnostics get the same enrichment.dsl-kit-mcp—DslMcpHandlerandDslMcpBuilder(flowed intoDslMcpServer) gainwith_suggester(SuggesterHandle) -> Self.dsl_kit_stepunknownmode,dsl_kit_explainunknowncode(which now uses the compactdid you meanform when the suggester fires, falling back to the fullKnown codes:dump otherwise), andDslMcpServer::call_toolunknown tool name all carry hints. Default isnoop_handle(), so the crate does not pull in a similarity algorithm on its own.dsl-kit-lint— new opt-inTypoHintrule. Takes a caller-supplied extractor closureFn(&A) -> Vec<(NodeId, String)>that decides what "labels" mean for the DSL;with_suggester(SuggesterHandle)wires in the plugin. ReportsSeverity::Infodiagnostics named"typo-hint"for every extracted label that is a near-miss but not an exact schema variant name. Not registered byLinter::with_defaults— same policy asDeadVariants.
Initial release.
dsl-kit-core— engine primitives: frame arena with an explicit spawn schedule,Seq/Par/Call/Scope/Maybeinterpretation, structured fan-out with join policies and reducers, cooperative cancellation, event stream, breakpoints that halt before a frame spawns, and a sans-io drive layer (drive/drive_async).dsl-kit-core— value semantics in the engine:Apply(pure op fold over anOpRegistry),Branch(value-dependent control flow via atruthyhook; the untaken side never spawns),Bind/Read(lexical env chain; unbound reads suspend as Call-shaped pendings the host answers throughresolve),Loop(per-iteration respawn through the spawn schedule, so breakpoints halt on every iteration boundary), andLit(literal leaves). Effects compose into value positions: anApplyargument can be aCallor a wholeParcascade.dsl-kit-core—DslSemantics+DerivedAst: the semantic half a DSL author writes (value type, unit, truthiness, binding storage) zipped with the derived classification into an engine-readyAst.dsl-kit-macros—#[derive(DslNode)](traversal),#[derive(DslSchema)](type-level schema extraction),#[derive(DslBuild)](ParseTree → typed AST, with#[dsl_build(with = ...)]per-field converters),#[derive(DslExec)](variant → engineNodeKindclassification via#[dsl_exec(...)]annotations:value/read/apply/bind/branch/repeat/seq/scope/maybe/call).dsl-kit-schema— schema reflection types consumed by parsers, editors, and AI clients.dsl-kit-parse— parser trunk:ParseTree, schema conformance, JSON front-end (serde_bridge), PEG interpreter, grammar generation from schemas (schema_gen, with per-fieldSyntaxOverrides), static grammar checks (grammar_check), and parse-guaranteed example synthesis (example_gen).dsl-kit-lint— walk-driven, schema-aware, author-extensible lint framework.dsl-kit-mcp— stdio MCP framework exposing a debugger-style tool surface (step/breakpoint/state/resolve/schema/lint) over anyDslHost, plus aDslMcpBuilderpath for authors who want to mix typed-fn tools (schemars-derived input schemas) with DSL-backed tools (tool_from_host) in one server. Both paths emit MCP 2024-11-05 spec-compliantinputSchema(type: "object"even for no-arg tools), so strict clients like Claude Code accept the whole tools/list response.dsl-kit— facade crate re-exporting the kit surface.dsl-kit-cli— developer entry-point binary shipping amcpsubcommand:cargo install dsl-kit-cli && dsl-kit-cli mcpstarts a stdio MCP server hosting a minimal built-in reference DSL (Lit / Var / Add / Mul / Let / If) through thedsl-kit-mcpframework, so developers get a working stepper without writing a host first. Room for more subcommands (lint / schema / grammar) as the CLI grows.- Examples (not published):
flow-example(orchestration DSL with engine round trip),expr-example(expression DSL with text and JSON round trips, interpreted entirely by the engine — no hand-written evaluator),custom-mcp-example(shipping a DSL as its own MCP server).