Changelog

Changelog

All notable changes to keiro-dsl are recorded here. The format follows Keep a Changelog, and the package follows the Haskell Package Versioning Policy.

[Unreleased]

0.5.0.0 — 2026-07-31

Breaking Changes
  • DiagnosticCode gains nine constructors: WorkspaceMemberUnreadable, WorkspaceMemberParseFailed, WorkspaceContextMismatch, WorkspaceAuthorityConflict, WorkspaceDuplicateDeclaration, WorkspaceDuplicateNodeName, WorkspacePathCollision, OwnershipMoved, and WorkspaceAuthorityChanged. Additions are append-only, but exhaustive matches over the type must be extended.
  • Keiro.Dsl.ScaffoldRun.Refusal gains GoldenRootDivergence, raised only by the workspace path when a golden payload fixture sits beside a member that the one workspace golden root lacks.
  • Keiro.Dsl.ScaffoldRun.WriteDisposition gains Unchanged, produced only by the workspace write path when a Generated module's bytes already match.
  • Keiro.Dsl.DiffReport.Remedy gains RemedyRescaffoldWorkspace.

No behaviour of the single-file path changed: it keeps its context-keyed record and manifest names, its report bytes, and its Overwritten disposition, and no existing generated bytes moved.

New Features
  • Adds service workspaces: a .keiro-workspace manifest names a service and lists its member .keiro files, and keiro-dsl check <manifest> validates them as one service contract. Shared ids, enums, rules, and mapped structural types resolve once across all members, so an aggregate in one file may use a declaration or feed a read model owned by another. A single .keiro file is unchanged and behaves as a one-member workspace.

    Membership is a set: member paths are normalized and canonically sorted, so listing order changes neither the parsed manifest nor any output. Composition refuses, before producing a graph, when members declare different contexts, when a member's module/layout clause contradicts the manifest authority, when a shared declaration or node is owned by two members (identical duplicates never silently merge), or when two members claim generated module paths that collide under case folding. One diagnostic can cite several files: the primary location keeps the established <file>:<line>: error[<Code>]: <message> shape and each further location follows as an indented note: line.

    check --emit, --explain-bindings, --coverage-report, and --fail-on-opaque all work against the merged whole-service graph, and keiro-dsl parse <manifest> round-trips the manifest canonically. Dispatch is by file extension, so every single-file branch is unchanged.

    New Keiro.Dsl.Workspace module; Keiro.Dsl.Validate.nodeIdentity is now exported. Recorded as ADR-14.

  • Whole-workspace scaffolding. keiro-dsl scaffold <manifest> --out DIR emits the complete generated module set for every member in one invocation, with unchanged flags. The set is emitted once from the merged spec, so the structural projection facade and the replay-audit assembly are produced exactly once from the complete graph, and every existing refusal gate runs over the whole set. Both preflights — golden fixtures stranded beside a member, and Generated paths lacking the @generated banner — are evaluated across the whole workspace before the output directory is created, so a failure in any member leaves the tree, the record, and the build manifest byte-for-byte untouched.

    History is workspace-keyed (workspace.<service>, a name a context can never collide with), and each record row carries its producing member, so an aggregate moved between member files is reported as an ownership move with zero stale churn and zero content change. A Generated module whose bytes already match is reported (unchanged) rather than rewritten, making idempotence observable. Ownership is attributed structurally through the new scaffoldStructuralOwners and bindingSkeletonOwners seams in Keiro.Dsl.Scaffold, never by parsing the human-readable origin string.

    New Keiro.Dsl.WorkspaceRecord and Keiro.Dsl.WorkspaceScaffold modules; Keiro.Dsl.ScaffoldRun now exports the gates and helpers the workspace path reuses (pureRefusals, missingGeneratedBanners, staleAgainst, constraintPlan, mappingDrift, newBindingObligations, obligationKindLabel, renderMappingIdentity) so the two paths cannot diverge on what counts as a refusal.

  • Adoption of pre-workspace output. The first whole-workspace scaffold into a directory that already holds per-context output imports what is attributable, reports everything, and claims nothing silently. A file is claimed only with evidence — record when a legacy per-context record for this workspace's effective context lists it, or banner when it sits at a planned Generated path carrying the @generated banner while no surviving record lists it. Hole paths are never claimed; everything else is listed as unclaimed and left untouched, and a bannerless file at a planned Generated path still refuses the run. Nothing is deleted or renamed: the superseded legacy record gains one appended superseded-by: line that its own v1 parser ignores. The migration report is persisted once as keiro-dsl-migration-report.workspace.<service>.txt. New Keiro.Dsl.WorkspaceAdoption module; recorded as ADR-15.

  • Whole-workspace diffing. keiro-dsl diff <manifest> --since <rev> composes the historical workspace from the manifest and member blobs at that revision through the same loader and diffs it against the working tree as one service. Membership deltas are covered, and when the manifest itself does not exist at the old revision an adoption baseline is composed from the current membership and flagged in the report.

    Findings from the merged graph are annotated with the member file and line that own the changed declaration and each use site, so a shared declaration's blast radius is visible across files. The report carries workspace metadata (service identity, manifest, since, old and new membership, adoption-baseline flag), and replay-impact and coverage output are merged across members. Owner-map and manifest-authority changes are reported as the non-blocking consumer-build advisories OwnershipMoved and WorkspaceAuthorityChanged, classified independently of wire evolution so they never mask a real wire finding.

    The keiro-dsl/diff-report/1 JSON schema is preserved: single-file reports keep their original bytes, and workspace inputs add a top-level workspace object plus optional per-finding declaration and useSites keys, which version-1 readers ignore. New Keiro.Dsl.WorkspaceDiff module; new Keiro.Dsl.DiffReport exports OwnedSite, WorkspaceChange, WorkspaceMeta, WorkspaceDiffReport, and workspaceDiffReport. ADR-4 is amended with the composed-workspace diff boundary.

0.4.0.1 — 2026-07-28

Other Changes
  • Adds PVP upper bounds to every dependency that previously carried a lower bound only, so cabal check reports no packaging warnings. No API or behaviour change from 0.4.0.0, which was tagged but never published.

0.4.0.0 — 2026-07-28

New Features
  • Structural scaffolding now creates hand-owned binding/fixture/initial skeletons grouped by their declared owner modules, persists granular hole obligations for non-overwriting re-scaffold reports, derives Generic for private shape types, and provides check --explain-bindings for deterministic package/module/signature/use-site reports.

  • Adds Keiro.Dsl.CodecCompare, a historical-codec comparison engine that classifies RFC 8785 canonical-JSON parity between a declared codec and a historical one, reports structured migration differences and declared-versus- observed branch coverage gaps, and writes stable reports atomically. scaffold --codec-comparison MAPPED-NAME --comparison-out FILE emits a non-production comparison module and runner for one structural mapped type.

  • Adds Keiro.Dsl.Coverage and reporting-only --coverage-report FILE on both check and diff, recording structural and opaque mapped-root coverage as JSON. check --fail-on-opaque fails when a private persisted root still contains an opaque boundary; diff --fail-on-opaque-increase fails when a change adds a named opaque boundary. Without the flags, coverage is purely informational.

  • Adds checked mapped structural and mapped opaque declarations with a resolved, total type-expression graph. Validation rejects recursive, ambiguous, non-injective, or incomplete mappings; recursive diff findings carry six-surface compatibility vectors and complete command/event/register use-site paths.

  • Structural consumer-type scaffolding now emits private Generated.<Context>.Structural.Shape.* wire representations, generates structural codecs from declared keys/defaults/tags, delegates opaque values only at their declared JSON boundary, imports consumer types into aggregate domains, and exposes eligible total scalar getters through a narrow Keiki 0.4 StructuralProjections witness facade.

  • Scaffold preflight now reports consumer packages/modules, refuses consumer modules inside the generated namespace before writing, includes consumer requirements in manifests, persists canonical structural/opaque mapping identities as forward-compatible JSON rows, and reports mapping drift on subsequent runs. Mapped register wire/binding/initial identities now participate in the aggregate fold fingerprint.

  • Generated harnesses exercise both structural binding laws, declared codec policy and current payload goldens, enum/union/optional fixture coverage, canonical projection witness agreement, and forward-versus-replay equality over every mapped and scalar register. The committed structural conformance suite includes opaque-boundary checks and three falsifying mutations.

  • Keiro.Dsl.ReplayImpact and diff --replay-impact-out FILE. Diffs now print whether stored-data replay is unchanged and can emit a stable JSON affected set for targeted auditing. New aggregates, events, transitions, and syntactically proven guard loosenings are replay-neutral; changed decode/fold surfaces identify conservative event types and whether snapshot-bearing streams must be included.

  • Scaffolding now emits one context-wide Generated.<Context>.ReplayAudit module. Its typed target list includes every aggregate, validates discovered stream names against the generated category, and documents the replay-neutral/targeted/full deployment tiers.

  • diff now emits non-breaking, coded advisories when a router resolve or dispatch surface, process handle surface, or unversioned process-timer payload changes. The advisories explain the subscription drain and dead-letter procedure needed to avoid mixed old/new deterministic fan-out during a deployment.

  • First-class replay-only transitions for guard evolution (plan 143). A replay-only prefix on a transition line marks it as serving inversion only: the parser accepts it, the pretty-printer round-trips it, and the scaffolder lowers it to B.replayOnly (keiki's ReplayOnly edge mode) in the transducer skeleton. New validator rules: ReplayOnlyEmitsNothing (error — a replay-only transition with no emit can invert nothing) and ReplayOnlyCommandStillLive (warning — no live sibling for the (source, command) pair; the fuller procedure is event retirement). A deprecated event may keep being emitted by a replay-only transition — replay-only transitions are not the write path.

  • diff computes the guard-tightening remedy (AggGuardTightened advisory): on any live-transition guard change without a replay-only twin, it prints a paste-ready replay-only twin whose guard is the removed region old ∧ ¬new, negation eliminated inside the guard grammar by the new total Keiro.Dsl.Grammar.complementExpr (De Morgan, comparison flipping, x == false for bare boolean atoms). The twin carries the old transition's writes/emits/goto and re-parses as-is; it is printed, never auto-applied.

  • Keiro.Dsl.PrettyPrint.renderTransition renders one transition in concrete .keiro syntax (used by the advisory).

  • Requires keiki >=0.4 && <0.5, including EdgeMode and the typed structural projection contracts.

0.3.0.0 — 2026-07-14

No user-facing changes. keiro-dsl is released at 0.3.0.0 to stay in lockstep with the rest of the Keiro package set.

0.2.0.0 — 2026-07-13

Breaking Changes
  • The process saga clause is now saga <Aggregate> category "<camelCase>", replacing saga <Aggregate> stream="<prefix>-" <> correlationId. Categories are validated (SagaCategoryIllegal) against the same rules as Keiro.Stream.category: non-empty, not $all, no - (Kiroku's category/id boundary), no whitespace or control characters, and no : (reserved for the wf:<name> workflow stream family). Generated process and router fills now build saga streams with entityStream and a typed category constant instead of splicing a string prefix.
  • process nodes must now declare node-level rejected => <policy> and poison => <policy> clauses (halt, deadLetter, or skip), and every timer fire disposition must carry an on-ambiguous arm alongside on-ok, on-reject, on-error, and not-mine. Mapping on-ambiguous => Fired is rejected (AmbiguousMarkedBenign): CommandAmbiguous means several aggregate edges matched, and is never a benign success.
  • Identifier lexemes are restricted to ASCII, and the validator now enforces per-category Haskell hygiene: constructor-position names must be constructor-safe (IdentNotConstructorSafe), no identifier may be a Haskell keyword (IdentHaskellKeyword), and generated vertex constructors may not collide (VertexCtorCollision). Specs that previously scaffolded to uncompilable Haskell are now rejected at check.
  • All numeric literal sites parse through Integer and fail with a positioned diagnostic when the value exceeds maxBound :: Int, instead of silently wrapping. maxBound itself still parses exactly.
  • Duplicate singleton blocks and duplicate goto clauses are reported at their second occurrence rather than being silently accepted with a last-one-wins reading; a missing goto is now anchored to its transition.
  • Text registers require a quoted initial, and a register whose type is an enum or the aggregate's vertex type must start at a declared constructor or state. The scaffolder refuses the run rather than emitting a broken initial.
  • Validation is substantially stricter, so specs that checked under 0.1.0.0 may now be rejected: duplicate node, command, event, enum-constructor, enum-wire and id-prefix names; dangling aggregate, projection, timer, workflow, signal and operation references; non-total or unresolved rule domains; duplicate workflow replay labels; incomplete or shadowed disposition tables; duplicate and dangling status-map keys; emit/intake topic-affinity mismatch; workqueue queueRef/table/DLQ divergence; unresolved dispatch dedup queues and fields; invalid process timer ceilings; and write targets that are not registers.
  • keiro-dsl scaffold now plans the entire module set before writing any byte, and refuses the run on module-path collisions, self-firewall breaches, or unlowerable spec content. It also refuses to overwrite a file on a Generated path that lacks the @generated banner; pass the new --force-generated-overwrite flag to override.
  • keiro-dsl diff gained a WARNING: tier, and its change lines now read <node> <facet> <subject>: <detail> rather than always saying event. Only BREAKING: changes exit non-zero.
New Features
  • Added a first-class readmodel node declaring table, schema, a typed columns block, version, captured shape, consistency, scope, feed (inline or subscription), and subscription. The validator owns a closed column-type vocabulary (text, int, bigint, bool, timestamptz, jsonb, numeric), detects shape-hash drift against the declared columns, rejects Strong + inline (no subscription worker advances the cursor a strong read waits on), rejects scope without Strong, and rejects an inline model that no aggregate projection feeds. scaffold emits ReadModel and ReadModelTable generated modules plus ReadModelHoles. Aggregate projection consistency is now optional and must agree with the read-model node when one exists; a projection with no readmodel node warns that registration, schema identity, and rebuild helpers are unavailable.
  • Query operations and PGMQ dispatch source/dedup read-model references now actually resolve against declared nodes — these were explicitly deferred no-ops in 0.1.0.0. Query consistency must be Strong, Eventual, or PositionWait, and a dispatch dedup field must be a column of the named model.
  • Added the router node for stateless content-based routing: input, key, resolve stable via read-model <name> row { ... } (or via hole), target, projections, dispatch-each, a runtime-owned dispatch-id strategy keyed on target stream name and occurrence, and node-level rejected/poison policies. Validation resolves the target aggregate, its command and command fields, the key field, and the read-model reference, and confines dispatch bindings to the input. and resolved. scopes plus quoted literals. scaffold emits Router/RouterHarness generated modules with RouterHoles/RouterValue stubs, and keiro-dsl new router prints a starter.
  • Added node-level rejected and poison worker policies on process and router nodes, reconciled against the per-dispatch on-failed arms: a dispatch declaring DeadLetter under a node that does not is a PolicyContradiction, divergent on-failed actions within one node are rejected because the runtime applies a single RejectedCommandPolicy to the whole failure group, and an unacknowledged rejected => deadLetter warns.
  • Added aggregate snapshot every <n> / snapshot on-terminal with a captured state-codec version=<n> shape-hash="..." fixture, lowered into the generated aggregate modules and pinned against the live runtime. Intervals below 1, and empty or zero-version codec fixtures, are rejected.
  • Added workqueue ordering (unordered, fifo-throughput, fifo-round-robin), group key from <field> via <fn>, and provisioning (standard, unlogged, partitioned). The validator rejects FIFO without a group key, a group key without FIFO, and an empty partition spec, and flags unlogged durability; the generated QueuePolicy is pinned to the live pgmq configuration types.
  • Added intake persist = full-envelope | dedupe-only, lowered to the live InboxPersistence value and wired through runInboxTransactionWith.
  • Added a durable-workflow evolution surface: guarded patch <id> { ... } blocks and a terminal continueAsNew <SeedType>, lowered into the workflow run options. Duplicate and malformed patch ids, and a non-terminal continueAsNew, are rejected.
  • Added a partial token on status-map, so an intentionally incomplete event-to-status mapping is expressible instead of failing the totality rule.
  • Publisher backoff accepts optional max= and multiplier=, and exponential backoff now lowers to ExponentialBackoff ExponentialBackoffOptions{..} with faithful duration units.
  • Rebuilt diff on an exhaustive node-family registry: every Node constructor either has a differ or carries a recorded out-of-scope rationale, so a new node kind can no longer be silently treated as safe. Classification now covers aggregate decode evolution through command indirection (field type, removal, wire spelling, enum add/drop, schema-version regression, old-anchored upcaster continuity, undeprecation), contract topic/schema-version/discriminator/field/ event changes, workqueue payload, identity, ordering and provisioning changes, process input and timer-window changes, intake decode posture, dedupe identity and persistence, emit mapping and ordering, publisher policy, workflow shape, body, patch and continue-as-new seed changes, read-model version/shape/feed/consistency evolution, projection and id-prefix re-keying, router stable-name changes, and dispatch retargeting.
  • scaffold now writes a versioned per-context record (keiro-dsl-scaffold-record.<context>.txt) and reports still-present modules made stale by node, layout, or --module-root changes. It never deletes generated or hand-owned files.
  • Diagnostics carry row-level source locations, so validator-owned rows (dispositions, status maps, timers, dispatches) are anchored precisely instead of at the enclosing node.
Bug Fixes
  • String literals now decode and re-render the closed DSL escape set, so topics, emit maps, and quoted field bindings survive a parse/pretty-print round trip instead of being corrupted.
  • The scaffolder escapes payload literal splices, closing a template-injection path where a quoted spec literal could break out of the generated Haskell string. Its status-map and harness lowering is now total.
  • Generated Stream and stream-category imports are emitted in the repository-standard post-qualified (ImportQualifiedPost) form.
  • The self-firewall is derived from Keiki's exported surface as a single token-aware scan, rather than a hand-maintained substring list that both missed operators and produced false positives.
Other Changes
  • Added conformance suites that round-trip every node family, compile every keiro-dsl new <kind> starter, cold-start the new read-model, router, snapshot, queue-ordering, and workflow-rotation surfaces, and exercise the generated read-model and router modules against the live runtime.
  • Test fixtures and conformance corpora resolve from either the package root or the repository root.

0.1.0.0 — 2026-07-05

Initial Hackage release.

Breaking Changes
  • Renamed the typed-spec file extension from .kdsl to .keiro before the first public release.
New Features
  • Added grammar, parser, pretty-printer, validator, diff engine, scaffold generator, harness emitter, and CLI for typed .keiro service specs.
  • Added aggregate, process manager, durable timer, integration contract, inbox, publisher, PGMQ workqueue, workflow, and operation nodes.
  • Added configurable module placement, build-wiring manifests, self-firewall checks, post-scaffold reports, per-iteration ergonomics, and new <kind> starter skeletons.
  • Generated validated event streams compatible with Keiro's command boundaries.
Bug Fixes
  • Tolerated formatter comma style in scaffold conformance tests.