8000 dsl-kit/CHANGELOG.md at main · ynishi/dsl-kit · GitHub
[go: up one dir, main page]

Skip to content

Latest commit

 

History

History
650 lines (606 loc) · 36.5 KB

CHANGELOG.md

File metadata and controls

650 lines (606 loc) · 36.5 KB

Changelog

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.

[Unreleased]

Fixed

  • dsl-kit-parse — schema_gen no 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 an Optional / Many / Map slot not declared non_empty — is an optional clause at its own position. In() for In { field: String, values: Vec<String> } is a parse error instead of a later conformance failure, and example_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_gen renders %str_raw (the built-in Vec<String> production) as a string literal instead of leaking the token name into the example, and reports any other unknown % pattern with the new example_gen::unknown_token diagnostic.

Added

  • dsl-kit-schema — new json_schema module: NodeSchema::to_json_schema(&TypeMap, RefStyle) renders the JSON front-end's document shape as JSON Schema 2020-12 — one oneOf arm per variant with type pinned by const, additionalProperties: false, required from the conformance rules, payload fields through the built-in mapping (String / integers / bool / Option<..> / Vec<String>) or a caller-supplied TypeMap, child slots by multiplicity (non_empty → minItems / minProperties

    • required), declared scalar shorthands as extra alternatives. RefStyle::Defs wraps a standalone document; OpenApiComponent yields the bare union for components.schemas. An unmapped payload type is a loud Error::UnsupportedField. A dsl-kit-parse integration test pins accept / reject agreement with serde_bridge.
  • 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 on to_json_schema, lowered to a parameterized SQL WHERE clause. No engine, no MCP. Its Eq.value shows the heterogeneous-scalar hook pair (SyntaxOverrides + #[dsl_build(with)] / #[dsl_dump(with)] + TypeMap) in the parser-only setting.

  • dsl-kit-parse — new dump module: DslDump, the inverse of DslBuild. A typed AST re-emits the ParseTree shape its own build derive accepts (to_parse_tree / to_parse_tree_with(&AllowTable), the latter carrying $allow annotations back into the tree), and dump_canonical_json chains the emitted tree through to_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: id is never serialized (re-parsing mints fresh ids), absent Options omit their keys, Vec payloads/slots always emit ([] included), empty keyed maps omit the slot, BTreeMap iteration order satisfies the keyed-slot sortedness gate. Serialization failures report the new serde_codes::DUMP_FIELD diagnostic.

  • dsl-kit-macros — #[derive(DslDump)] generates the reverse walk for the same enum shape DslBuild accepts, so the pair round-trips by construction (from_parse_tree(&ast.to_parse_tree()?, &ids) rebuilds an equivalent AST modulo NodeIds). 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, _>), whose with output cannot conform. Duplicate with annotations 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, and call(label, payload = args) passes that one field unwrapped. Payload fields must implement serde::Serialize; a serialisation failure is reported as {"__payload_error": "…"} rather than silently dropped. call(label) is unchanged and still suspends with null.

  • dsl-kit-core — Stepper::suspended_call_spec returns the whole CallSpec (label and payload) for the sole in-flight Call; suspended_call stays as the label-only convenience.

  • dsl-kit-mcp — PendingProjection::of, SuspendedCall::sole and HostLocation::of project 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 — new import module: a load/link phase that lets a document pull in other sources before conformance and DslBuild. The JSON bridge reserves {"$import": "name"} at node positions as an import placeholder; import::load_json_str / import::load_json_value expand placeholders to a fixpoint through a caller-supplied SourceResolver (the kit stays io-free — the bundled MapResolver serves named in-memory sources, filesystem access is a host-side impl). Specifiers are literal-only, so Loaded::dependencies reports the full import graph without executing anything. Cycles are detected via a Pending cache sentinel and rendered as the full chain (<root> → a → b → a); ImportLimits caps depth / source count / total bytes with a dedicated diagnostic per bound; fetch/parse failures are cached and replayed per importing site with an in_import chain-context marker. A placeholder that skips the loader is rejected at check_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 the node choice every child slot references, so the spelling works at every node position; a non-Choice start rule is wrapped). Opt-in by design — untouched grammars accept no import syntax, example_gen never spells @import in synthesized examples, and grammar_check's schema-consistency pass exempts the reserved $import node. The loader grows a builder form (import::Loader::new(schema) .with_grammar(&g).with_limits(l)) with load_text / load_json_str / load_json_value entry points; ImportSource::Text sources parse through the configured grammar and mix freely with JSON sources in one load (a text source arriving with no grammar configured is a loud import::text_unsupported failure). MapResolver::insert_text registers 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_json consumes the wire-shape bundle ({"name": {"json": "…"} | {"text": "…"}}, flavour always tagged, problems collected as import::bad_sources). The dsl_kit_load MCP tool takes an optional sources object: when present the load routes through the new DslHost::load_json_bundle hook (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-host implements the hook end to end (JSON + text sources through the schema-generated grammar with add_import_syntax), and cfg-example gains an import-loader demo section (root + 2 sources linked into one document).

  • dsl-kit-schema — new ChildValueShape enum (Recursive / Scalar { ty }, #[non_exhaustive]) plus a value_shape field on ChildSchema that pins what a keyed slot's values are, not just their cardinality. Historical shapes (One / Optional / Many, plus keyed slots spelled BTreeMap<String, Self> / BT EA3D reeMap<String, Box<Self>>) report Recursive and keep the pre-0.6 NodeSchema::to_json layout — no value key 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 with Scalar { ty: "String" } etc. and the JSON layout gains a "value": { "kind": "scalar", "type": "…" } object. ChildSchema::recursive(name, mult) and ChildSchema::scalar_map(name, ty) are convenience constructors for hand-written schemas. Breaking for hand-written struct literals of ChildSchema — one added value_shape field to fill in; the recommended migration is to switch to ChildSchema::recursive(name, mult) / scalar_map(name, ty) rather than referencing ChildValueShape directly.

  • dsl-kit-macros — #[derive(DslNode)] / #[derive(DslSchema)] / #[derive(DslBuild)] now recognise BTreeMap<String, V> where V is a scalar payload (String, i64, bool, f64, …) as a scalar keyed slot. The schema reports Multiplicity::Map and ChildValueShape::Scalar { ty }; Walk / WalkMut skip the slot (scalar values are not Self); DslBuild reads the tree's keyed half through the new build_scalar_map::<V> helper and rebuilds a BTreeMap<String, V> with keys intact. Existing recursive keyed shapes are untouched.

  • dsl-kit-parse — build_scalar_map<V> helper for hand-written DslBuild impls: reads ParseTree::keyed_children[slot], treats each entry as a leaf tree whose single field is named "value", and delegates payload conversion to build_field so the same serde::de::DeserializeOwned + FromStr bounds apply. Missing "value" fields surface as codes::MISSING_FIELD, type errors as codes::FIELD_TYPE, and duplicate keys as codes::DUPLICATE_KEY — the same envelope as the recursive build_child_map.

  • dsl-kit-parse — JSON ⇒ ParseTree bridge accepts scalar keyed slots. When a schema declares Multiplicity::Map with ChildValueShape::Scalar { .. } the bridge routes the JSON object through build_scalar_keyed_slot, wrapping each value as a value-field leaf so build_scalar_map can 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_schema routes each ChildValueShape::Scalar { ty } entry through the built-in payload mapping (String → %str, integer → %int, bool → true|false, and the Option<String> / Vec<String> entries the payload path already recognises) and wraps the value in a synthetic value-field leaf so build_scalar_map reads it back the same way as the JSON path. grammar_check treats empty-variant Peg::Node as the scalar-entry wrapper marker rather than an unknown variant. Scalar value types outside the built-in mapping still fail loudly at pre-flight with schema_gen::codes::UNSUPPORTED_MAP_VALUE_SHAPE; register a SyntaxOverrides value 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 recognise BTreeMap<String, T> and BTreeMap<String, Box<T>> (where T is the derived-on enum itself) as keyed self-recursive child slots. Walk / WalkMut iterate .values() / .values_mut() in the map's own (sorted-by-key) order; the schema reports Multiplicity::Map. #[derive(DslBuild)] reads the tree's keyed half through build_child_map and rebuilds the BTreeMap with its keys intact; #[derive(DslExec)] collect-children treats keyed slots the same as Vec<Box<T>> for ordering. Non-Box keyed 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_children holds (slot, [(key, subtree)]) alongside the positional children, with a ParseTree::keyed_child_slot(name) accessor; build_child_map is the DslBuild helper that turns a keyed slot into a BTreeMap<String, T>. check_conformance accepts zero-or-more keyed entries and rejects the ways a tree can get keying wrong (see the diagnostics below). The JSON ⇒ ParseTree bridge reads a Map slot 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 how serde_json was 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 a Map slot is supplied as a positional list or a positional slot as keyed entries, which would otherwise read as an empty slot; and codes::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 — new Multiplicity::Map variant marking a string-keyed child slot (Map<String, V> shape at the Rust level). Multiplicity::as_str() returns "map" and NodeSchema::to_json emits "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 the Many idiom'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 %ident or a %str literal, 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_schema therefore no longer refuses Multiplicity::Map schemas, and schema_gen::codes::MAP_NOT_IMPLEMENTED is gone with the last of its siblings.

  • dsl-kit-parse — Peg::KeyedEntry { slot, key, value } plus the peg::keyed_entry constructor: the capture primitive behind the syntax above. A separate primitive rather than a Field convention because a Field binds one fixed name to its productions, which cannot express a name the input supplies. Peg::Node sorts each keyed slot before emitting, so hand-written grammars get the canonical order without arranging for it. Breaking for out-of-crate match on Peg, 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_MULTIPLICITY slugs backing the #[non_exhaustive] catch-all arms; surface a stable signal instead of a panic when a future Multiplicity variant is added ahead of parse-side support.

  • examples/cfg-example — new example crate (cfg-dsl / cfg-host / cfg-mcp plus a stdout demo) demonstrating the keyed Map primitive end to end. Cfg is a small configuration DSL whose children are named rather than positional: Env carries BTreeMap<String, Box<Cfg>> and Overrides carries BTreeMap<String, Cfg>, so both derive arms (MapBoxed and Map) stay covered by one DSL, while Ref suspends for a host-supplied value and Leaf terminates. 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_schema reporting multiplicity: "map", dsl_kit_load accepting a keyed document, dsl_kit_step / dsl_kit_resolve walking it). cfg-mcp installs with cargo install --path examples/cfg-example/crates/cfg-mcp.

Changed

  • dsl-kit-mcp — SuspendedCall and PendingProjection gained a payload: serde_json::Value field, and the dsl_kit_state / dsl_kit_pending tools now emit it under "payload". A client answering a Call reads 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::sole are the construction paths that survive future additions.

  • dsl-kit-schema — Multiplicity is 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 on Multiplicity without a catch-all.

  • dsl-kit-parse — ParseTree gained the keyed_children field. Breaking for front-ends that construct ParseTree with a struct literal; ParseTree::new(variant) followed by field assignment is the construction path that survives future additions. The struct is deliberately left exhaustive (unlike Multiplicity) 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 [].

Deprecated

Removed

Fixed

  • dsl-kit-core — the engine no longer drops NodeKind::Call's payload. It reached the host as Null regardless of what the DSL attached to the node, contradicting CallSpec::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 what Call-shaped effects exist to avoid. The payload now travels verbatim into CallSpec. Present since the engine walker landed; the unbound-Read path (payload {"kind": "read"}) was the only one that ever exercised the channel.

  • dsl-kit-lint — NoEmptyManyChildren is now NoEmptyChildSlots (no-empty-many-children → no-empty-child-slots, dsl_kit::lint::no_empty_many_children → dsl_kit::lint::no_empty_child_slots), reports at Suspicious / Warn instead of Correctness / Error, and covers Multiplicity::Map slots alongside Many.

    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_conformance accepts empty, and the schema has no way to say "at least one". So a DSL with a legitimately empty block or argument list got an Error from 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 what Suspicious / Warn is 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::Error keep compiling while quietly seeing different results.

  • dsl-kit-schema — Multiplicity::Map's documentation claimed the derive recognises HashMap<String, Box<Self>> and that runtime support was unimplemented. Neither was true: only BTreeMap shapes 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.

Security

[0.4.0] - 2026-07-24

Added

  • dsl-kit-core — structured, Clippy-style fix suggestions in suggest. Applicability (MachineApplicable / MaybeIncorrect / HasPlaceholders, serde round-trippable, no Unspecified escape hatch) gates auto-apply; only MachineApplicable may be applied without review. FixSuggestion pairs a message with a multipart Vec<PatchPart> patch (PatchPart { node, path, replacement }, NodeId-anchored) and an Applicability. Immutable once built via FixSuggestion::new / with_part. The string-only Suggester / Suggestion contract is unchanged — this layer sits on top of it.
  • dsl-kit-lint — central lint declaration registry. LintCategory (Correctness / Suspicious / Style / Complexity / Contract) and LintDecl { name, code, category, default_severity, desc } decouple rule metadata from the Rule impl (rustc declare_lint! style). LINT_DECLS lists all seven built-ins; lint_decl(name_or_code) looks one up; lint_catalog() projects them into ErrorCatalogEntry. Lint codes use the dsl_kit::lint::<name> form, sharing the dsl_kit:: code space with engine error codes.

Changed

  • dsl-kit-lint — Diagnostic gains an Option<FixSuggestion> suggestion field (None for report-only rules; existing rules are unchanged). New LintContext::report_with_suggestion helper. TypoHint now attaches a FixSuggestion (MaybeIncorrect, single-part patch replacing the label with the top fuzzy candidate) on each near-miss. Breaking: struct-literal Diagnostic { .. } callers must add suggestion: None.
  • dsl-kit-mcp — dsl_kit_explain now merges the built-in lint codes (dsl_kit_lint::lint_catalog) into its catalogue alongside engine error codes and host-contributed entries; the unknown-code did you mean suggester runs over the merged code space. Adds a dsl-kit-lint dependency.

Deprecated

Removed

Fixed

Security

[0.3.0] - 2026-07-23

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 — FieldSchema gains a pub optional: bool. Set to true for payload fields whose absence is a valid tree shape (typically Option<T> → None, Vec<T> → empty). to_json emits "optional": true only when set, preserving the pre-0.3 layout for required fields. Helper constructors FieldSchema::required / ::optional cut boilerplate for hand-written schemas. Breaking: struct-literal callers must add optional: <bool> to every FieldSchema { ... } site.
  • dsl-kit-macros — #[derive(DslSchema)] sets optional: true automatically for payload fields typed Option<T> / Vec<T> (where T is not the enum itself). #[derive(DslBuild)] routes those types to new build_field_optional / build_field_vec helpers by default (no #[dsl_build(with = ...)] needed for plain Option<String> / Vec<String> / any Option<T> / Vec<T> where T: DeserializeOwned + FromStr for Option). #[dsl_build(with = path)] on an optional payload short-circuits to None / vec![] when the field is absent, so hand-written converters no longer need to defend against missing keys.
  • dsl-kit-parse — check_conformance skips the MISSING_FIELD diagnostic for optional: true fields; the pair-hint pass in missing_slot_names skips them too so did you mean X (missing) never mislabels an optional slot. New public helpers build_field_optional::<T> / build_field_vec::<T> handle every canonical shape of absence: missing field, JSON null, canonical text none, empty bracketed list.
  • dsl-kit-parse::schema_gen — built-in canonical-syntax mappings added for Option<String> (none | %str) and Vec<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_conformance remains the authority on required / duplicate / unknown-slot diagnostics.
  • dsl-kit-parse::peg — new %str_raw token: same match as %str but contributes the raw source slice (quotes + escape sequences intact) so build_field_vec can hand the joined field text straight to serde_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_id is removed, and flow_syntax_overrides sheds its Option<String> entry. The built-in Layer 2 mapping now covers the shape end-to-end. Only policy: Option<JoinPolicy> (a non-String Option) still needs its SyntaxOverrides value production and parse_policy converter.

Default DslHost implementations for call-less DSLs — closes GH issue #3.

  • dsl-kit-mcp — DslHost::resolve and DslHost::step_to_done gain default bodies suited to hosts whose DSL never suspends on external calls: resolve returns the new RESOLVE_UNSUPPORTED_MSG constant, step_to_done drives step_to_yield (returning on Done / Suspended, so breakpoint and suspend semantics match step_to_yield exactly) up to a configurable step_budget() (default 4096) before erroring with a standardized budget-exceeded message. New supports_calls() hook (default true): hosts that override it to false get dsl_kit_resolve / dsl_kit_resolve_by_id gated at the handler with the same RESOLVE_UNSUPPORTED_MSG error. Existing hosts that override both methods are unaffected.

Normalized type names in schema / diagnostics — closes GH issue #4.

  • dsl-kit-macros — #[derive(DslSchema)] normalizes the FieldSchema.ty source 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 via strip_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 — new OwnedDerivedAst<L, S>, the owned counterpart of DerivedAst<'a, N, S>. OwnedDerivedAst::new(&root, sem) walks the tree once and projects each node's NodeKind classification and literal payload into an owned side table; the borrow of root ends when the constructor returns, so the value carries no lifetime. Implements Ast under the same bounds as DerivedAst (L: Clone + Debug, S: DslSemantics, S::Value: From<L>) and is Clone when L and S both are, letting a host keep a pristine copy to rebuild from on reset. DerivedAst is 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 — OwnedDerivedAst is re-exported through the crate's wholesale pub use dsl_kit_core::* facade.
  • dsl-kit-cli — the built-in reference host (RefHost) now owns its Ref program by value and builds its engine over OwnedDerivedAst; the two Box::leak sites (default program + load_json) are gone, and load_json drops the previous program instead of leaking.
  • examples/expr-example — ExprAst is now OwnedDerivedAst<<Expr as DslExec>::LitValue, ExprSemantics> (no lifetime); ExprHost owns its Expr and drops both Box::leak sites. expr_engine returns a 'static Engine<ExprAst>.
  • examples/flow-example — worked example of the hand-written owned projection for a non-derive AST: FlowAst drops its lifetime parameter and stores each node's NodeKind by value (via the new flow_node_kind helper) instead of borrowing &Flow; FlowStepper and FlowHost shed their lifetimes and FlowHost owns its Flow program with no Box::leak.

[0.2.0] - 2026-07-22

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. FuzzySuggester implements the core Suggester trait via strsim ^0.11 with Jaro-Winkler default, Levenshtein / Damerau-Levenshtein switchable, threshold 0.7, and max_results 3. Depends on dsl-kit-core only; other kit crates never depend on it, so pulling in a similarity algorithm stays opt-in at the composition root.
  • dsl-kit-core — new suggest module owns the shared contract: Suggester trait (suggest + enrich_unknown default formatter), NoopSuggester zero-cost default, Suggestion { candidate, score }, SuggesterHandle = Arc<dyn Suggester>, and a noop_handle() constructor. The trait is &[&str]-only so plugins do not need to depend on NodeSchema / OpRegistry / any downstream type.
  • dsl-kit-core — OpRegistry and ReducerRegistry gain with_suggester(SuggesterHandle) -> Self. EngineError::UnknownOp and EngineError::UnknownReducer grow a hint: String field: empty by default preserves the historical unknown op OpId(...) / unknown reducer ReducerId(...) wording, populated by resolve() when a suggester is injected. matches! patterns keep working; explicit EngineError::UnknownOp { id } constructors need to add hint: String::new().
  • dsl-kit-parse — every unknown-* diagnostic now routes through the trait. check_conformance / from_json_value / from_json_str keep their signatures and use an internal BuiltinLevenshteinSuggester (case-insensitive Levenshtein, the same algorithm the crate has always shipped); new check_conformance_with / from_json_value_with / from_json_str_with variants accept a &dyn Suggester for injection. check_schema_consistency_with is the same idea on the grammar-check side. UNKNOWN_FIELD / UNKNOWN_CHILD also mark a suggested candidate (missing) when that slot is declared but currently absent — the pair-hint the design brief calls for on typo pairs like taget -> target. PEG's undefined-rule / unresolved-start-rule diagnostics get the same enrichment.
  • dsl-kit-mcp — DslMcpHandler and DslMcpBuilder (flowed into DslMcpServer) gain with_suggester(SuggesterHandle) -> Self. dsl_kit_step unknown mode, dsl_kit_explain unknown code (which now uses the compact did you mean form when the suggester fires, falling back to the full Known codes: dump otherwise), and DslMcpServer::call_tool unknown tool name all carry hints. Default is noop_handle(), so the crate does not pull in a similarity algorithm on its own.
  • dsl-kit-lint — new opt-in TypoHint rule. Takes a caller-supplied extractor closure Fn(&A) -> Vec<(NodeId, String)> that decides what "labels" mean for the DSL; with_suggester(SuggesterHandle) wires in the plugin. Reports Severity::Info diagnostics named "typo-hint" for every extracted label that is a near-miss but not an exact schema variant name. Not registered by Linter::with_defaults — same policy as DeadVariants.

[0.1.0] - 2026-07-22

Initial release.

  • dsl-kit-core — engine primitives: frame arena with an explicit spawn schedule, Seq / Par / Call / Scope / Maybe interpretation, 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 an OpRegistry), Branch (value-dependent control flow via a truthy hook; the untaken side never spawns), Bind / Read (lexical env chain; unbound reads suspend as Call-shaped pendings the host answers through resolve), Loop (per-iteration respawn through the spawn schedule, so breakpoints halt on every iteration boundary), and Lit (literal leaves). Effects compose into value positions: an Apply argument can be a Call or a whole Par cascade.
  • 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-ready Ast.
  • 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 → engine NodeKind classification 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-field SyntaxOverrides), 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 any DslHost, plus a DslMcpBuilder path 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-compliant inputSchema (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 a mcp subcommand: cargo install dsl-kit-cli && dsl-kit-cli mcp starts a stdio MCP server hosting a minimal built-in reference DSL (Lit / Var / Add / Mul / Let / If) through the dsl-kit-mcp framework, 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).
0