Living checklist for finishing the consolidation. Phase 1 (foundation) is done
and on main; this file tracks what remains. See the top-level README.md for
the why, and CLAUDE.md for the non-negotiables.
Legend: [ ] todo · [~] partial · [x] done ·
- Normative
schema/flow.v1.schema.json(JSON Schema draft 2020-12). - Cross-language conformance corpus
conformance/v1/(valid + invalid) with the frozen-case backward-compat contract. - TS package generates model types from the schema
(
json-schema-to-typescript); corpus validated withajv; green. - Rust crate generates model types from the schema at build (
typify,build.rsrewrites$defs→definitions); corpus validated withjsonschema+ deserialized into the generated model; green. - CI: drift guard + both test suites. Apache-2.0, README, per-dir READMEs.
Goal: the validators, hours math, parser, mutator, and engine live here — one copy per language, adapted to the generated types, covered by the corpus. Nothing is migrated in the consumer repos yet (that's Phase 3).
-
parse.ts— safe-subset YAML decode (anchors/aliases/tags/dup-keys/ implicit-typing rejection,nodeRangessource offsets). -
mutate.ts— comment-preserving edits (stays TS-only; the daemon never edits). Works on theyamlCST, not the generated types; only import from the model isComponentKind. 20-case suite ported. -
validate.ts— reachability BFS, caller-trap, exit-set exactness, DTMF, prompt length.⚠️ dangling-target code reconciled to the frozen corpus:unknown_exit_target→unknown_target. -
hours.ts—HH:MM/date/IANA-tz checks,open<close, no overnight. -
check.ts(parse+validate gate) andissues.ts(Issue,hasErrors,MISSING_ASSET). - Decide the fate of the model helpers currently in
model.ts(isTerminal,requiredExits,promptText,promptAudio,isGeneratedClipRef,requiredAssets): kept as hand-written functions layered on the generated types insrc/model.ts— they are logic, not shape. - Adapt every import from the old hand-written
model.tstosrc/model.ts(which re-exportssrc/generated/model.ts). - Port the existing
*.test.tssuites (parse,validate,refs); themutatesuite waits onmutate.tsabove.
-
validate.rs— the semantic twin ofvalidate.ts(agrees via corpus);ValidationError::code()maps to the shared code vocabulary (unknown_target, matching the frozen corpus). -
hours.rs— timezone/date math (time,time-tzdeps came along).validate_configis piece-based (unchanged);evaluatetakes&Node. -
engine.rs— interpreter + theFlowEffectsasync trait definition (the daemon keeps itsCallFlowEffectsimpl in its own repo). Matches on the generatedNodedirectly;goto/menu usenode.exits(). -
trace.rs— Serialize-only run trace (output format, not document shape);flow_versionisu64to match the generatedFlow. -
⚠️ Reconcile the generatedNodeshape. It was worse than the note assumed: typify emitted an#[serde(untagged)]enum withkind: serde_json::Value, which mis-discriminated (amenudeserialized as agreeting, silently droppingoptions) — the corpus only checked "does it deserialize at all", so it passed. Fixed inbuild.rs(Rust-only, no schema / generated-type hand-edits): inline the per-nodeoneOf$refs and rewrite thekindconst→single-valueenum, so typify emits#[serde(tag = "kind")]withexitsper variant. Hand-written helpers (kind,is_terminal,required_exits,exits,Prompt::as_text,Flow::from_yaml) live insrc/model_ext.rs— the Rust twin ofmodel.ts(logic, not shape). - Bring the
dev-dependencyontokiofor the engine's async scenario tests. (anyhow+async-traitalso came along, as deps — the publicFlowEffectstrait signature uses them.)
- Wire the
semanticfield of each*.expected.jsoninto both test suites (was: onlystructurallyValid). Subset match on both sides: acceptance matches (valid === semantic.ok) + every listed code reported, since a single defect can cascade (e.g. dangling→trapped). Rust maps serde parse failures to codes (missing_field,unknown_kind) alongsideValidationError::code(). - Add regression cases for the reachability/trap/exit-exactness rules and the
hours edge cases:
invalid/{unreachable,trapped,unexpected-exit, overnight-hours,bad-timezone}andvalid/hours-holiday(holiday exception override). Forced a cross-language fix: Rust wrapped all hours defects as onehourscode; nowValidationError::Hoursdelegates toHoursError::code()so both languages report the specific code (non_positive_range,unknown_timezone, …). (DST-specific corpus cases still worth adding later;hours.rs's DST math is unit-tested.) -
⚠️ Encode the one deliberate divergence: unknown fields are a non-blockingunknown_fieldwarning in TS and silently ignored by Rust serde — both still accept the document.valid/unknown-field+ an optionaltsWarningsfield in*.expected.json, asserted by the TS suite and ignored by Rust.
- Source shared constants. Decision: keep the ~4 semantic constants
(
SUPPORTED_SCHEMA_VERSIONS,MAX_PROMPT_CHARS,VALID_DIGITS) per-language with reciprocal twin pointers rather than add a shared codegen step for so few values. Component defaults are already single-sourced — they live in the schema, so typify reads them for Rust. - Add reciprocal cross-language pointers. Each side's constants name their
twin (
model.ts↔validate.rs/lib.rs); the Rust modules' headers already point back at the TS files. Fixed stalemodel.rsreferences inmodel.ts(the Rust helpers live inmodel_ext.rs).
Trigger: the format is stable enough and the repo is ready to be public.
- TS: real build via
tsconfig.build.json(ESM +.d.ts+ maps intodist/, schema JSON copied in),main/module/types/exportspoint atdist/,files: ["dist"],privatedropped,publishConfig.access: "public",prepublishOnly(typecheck + test + build),repository+ bundledLICENSE. Workspace dev flow unchanged (tests importsrc/). - Rust:
publish = falsedropped;cargo packageverify passes. The catch was self-containment, not manifest fields:build.rsandFLOW_V1_SCHEMA'sinclude_str!read../../schema/…, which doesn't exist in a packaged crate. Now a committed crate-localschema/flow.v1.schema.jsoncopy ships in the package;build.rssyncs it from the root schema in workspace builds (CI fails on a stale copy) and builds from it when packaged.tests/excluded from the package (the conformance suite needs the repo checkout). Keywords/categories added; no*dep pins existed anymore. CI now runs the TSbuildandcargo packageso neither artifact can regress. - Package/crate semver vs
schema_version: independent semver; supported doc versions advertised viaSUPPORTED_SCHEMA_VERSIONS(see README "Versioning"). Both artifacts sit at0.0.1and stay0.0.xuntil the consumer repos have adopted the published packages.
- (Superseded at 0.0.2: Rust releases folded into release-please — one
combined release PR for both packages,
cargo-workspaceplugin, samewavekat-flow-vX.Y.Ztag stream; release-plz removed.) Rust:release-plz(workflow shape fromwavekat-platform-client, on hosted runners like the rest of this repo's CI). Tagswavekat-flow-vX.Y.Z(crate-name prefix so the two tag streams in this repo can't collide). - TS:
release-please(manifest mode, as inwavekat-brand/wavekat-lab). Componentflow-schema, tagsflow-schema-vX.Y.Z; pre-major bump config keepsfeat:/fix:on0.0.xpatch bumps. Manifest starts at0.0.0so the first cut release is the as-yet-unpublished0.0.1. - Publish switches flipped (2026-07-19):
RELEASE_ENABLEDset, both0.0.1releases published — thewavekat-flowcrates.io name and@wavekat/flow-schemanpm scope are claimed. npm auth is trusted publishing (OIDC, no stored token; the npm package trustsrelease-please.ymlin this repo); crates.io auth is the org-levelCARGO_REGISTRY_TOKENsecret. - Add README badges (npm + crates.io + docs.rs), matching the
banner+badges header of the sibling crates (
wavekat-core,wavekat-turn). All three resolve now that both packages are published.
- Make the GitHub repo public.
- Publish
@wavekat/flow-schema(npm) andwavekat-flow(crates.io) — both at0.0.1, 2026-07-19 (published ahead of the repo going public). - Platform repo: delete
packages/flow-schema, depend on the published npm package. Import paths are unchanged, so this is a dependency swap. - Voice-daemon repo: replace the
crates/wavekat-flowpath crate with the registry dependency; bump. - Migrate doc 48 (
docs/48-ivr-call-flows.mdin the voice-daemon repo) intodocs/here; leave a pointer behind.
- Engine placement/timing. doc 48 wanted the Rust engine extracted only
after M2. It's coming in with Phase 2 here because the boundary is already
clean (no SQLite/UI/sync deps; the sole
FlowEffectsimpl is in the daemon). Confirm this is acceptable, or split the engine into a later step. - Should the model helpers (
requiredExits,isTerminal, …) be generated (via a schema extension / annotations) or stay hand-written twins? Leaning hand-written — they're small and are logic, not shape. -
schema_version2 planning: theassistantnode (doc 48 M3) will be the first additive bump — it lands asschema/flow.v2.schema.json+ a frozenconformance/v2/, with v1 untouched.