refactor(workspace): remove KV migration machinery#1519
Merged
Conversation
…with flat camelCase schema table.set() replaces entire rows atomically—dot-notation keys provided zero per-field conflict resolution benefit and forced bracket access. camelCase is consistent with every other table in workspace.ts; dot-notation is reserved for KV keys where per-key LWW matters.
Single blob meant LWW would overwrite service + model together on concurrent edits. Individual KVs give per-key resolution—changing the service on one device and the model on another both survive. Per-service model memory is preserved when switching between transcription services.
Only missing synced setting found during audit of settings.ts. OpenRouter model preference roams across devices.
Wave D audit found all types already correct—the two mismatches with settings.ts (retention.maxCount as number vs string.digits, temperature as number vs string) are intentional: the workspace uses semantically correct types while settings.ts has localStorage string artifacts. Wave E adds JSDoc explaining design rationale for flat row schema, per-service model memory, LWW key granularity, and intentional type differences with the old settings schema.
ASCII diagrams show where data lives today vs after migration, and the wave progression from schema polish through settings split, migration, and eventual sync wiring.
…schemas Replace flat nullable fields with arktype discriminated unions on `status` for `transformationRuns` and `transformationStepRuns` tables. Uses the `Base.merge(type.or(...))` pattern where `.merge()` distributes over each union branch. Unlike `transformationSteps` (which stays flat to preserve per-provider model memory across type switches), runs have one-way state transitions with no data to preserve across states.
Document why transformationRuns and transformationStepRuns use discriminated unions on status while transformationSteps stays flat. Contrasts the one-way state transition argument against Decision 1's bidirectional type switching.
…return type - Add required defaultValue field to KvDefinition type - Change KvHelper.get() return type from KvGetResult<T> to T directly - Update defineKv() overloads to require defaultValue parameter - Single-version: defineKv(schema, defaultValue) - Multi-version: defineKv(v1, v2).migrate(fn, defaultValue) - Distinguish schemas from values via '~standard' property check Completed spec items 1, 2
- get() now returns defaultValue on miss/invalid instead of KvGetResult union - Added observeAll() to KvHelper type and create-kv.ts implementation - observeAll() batches all key changes per transaction, skips invalid/unknown - Completed spec Tasks 3 and 4
…ersion schema detection Update all 43 KV entries in workspace.ts with correct defaults per old settings schema. Update all test files to pass defaultValue to defineKv(). Fix arktype schema detection in define-kv.ts: arktype schemas are functions (typeof === 'function'), not objects, so the isSecondArgSchema check now handles both types. This prevented multi-version defineKv(v1, v2) from working correctly.
KV settings are simple flags—not versioned documents. The 'invalid → return default' behavior already handles schema changes. The multi-schema variadic overload adds complexity nobody uses.
…Kv, and createKv - Simplify KvDefinition to single schema (no TVersions tuple, no migrate field) - Rewrite defineKv to single signature: defineKv(schema, defaultValue) - Remove variadic overload, isSecondArgSchema detection, createUnionSchema import - Simplify createKv get/observe/observeAll to return validated values directly - Remove parseValue helper and all definition.migrate calls from KV path - Update tests: remove variadic/migration tests, keep shorthand + validate-or-default - Completed spec items 1.1-5.3
- Rewrite workspace-api skill: KV section now shows defineKv(schema, default) only - Add scalar-per-key convention to skill file - Update versioned-schemas article: KV uses validate-or-default, not migration - Update API design decisions: explain deliberate Tables/KV asymmetry - Update static workspace guide: replace variadic KV examples with shorthand - Update index.ts module JSDoc: simplified KV example - Completed spec items 6.1-6.6
…section All success criteria met. 347 tests pass, 0 production code changes needed.
Update inline-single-use-definitions-in-tests.md to use the current defineKv(schema, defaultValue) signature. The old examples showed defineKv(type(...)) without a default, which no longer compiles.
…type KvDefinition now documents the full KV-vs-tables design rationale: why KV uses validate-or-default instead of migration, and how defaultValue serves dual duty (initial state + validation fallback). KvHelper methods (get, set, delete, observe) expanded with behavioral details and usage examples. Removed KvGetResult—the old three-status discriminated union from before get() was simplified to always return T. Zero internal consumers remained. Fixed stale defineKv examples in typescript SKILL.md that had _v discriminants and were missing the now-required defaultValue argument.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Simplifies the workspace KV system end-to-end: polishes the schema layer, adds required defaults so
get()always returns a typed value, then removes the migration machinery that defaults made unnecessary.This is Wave 1 of the Whispering workspace polish spec plus two follow-on specs that it unlocked. The three phases built on each other—schema polish exposed that KV defaults were missing, defaults made migration structurally unnecessary, and removing migration was the payoff.
KV defaults
defineKv(schema)had no concept of a default value.get()returned a discriminated union ({ status: 'valid', value } | { status: 'not_found' } | { status: 'invalid' }) that forced every consumer to handle three cases. For the upcoming settings separation (Wave 2), we needget()to always return a validT.The default is never written to Yjs—it would pollute CRDT history and cause initialization races on multi-device sync. Added
observeAll()for reactive observation of all KV entries. Updated all 44defineKvcall sites in Whispering to include defaults.This phase also exposed a real bug: the
isSecondArgSchemaheuristic usedtypeof args[1] === 'object'to distinguish schemas from default values, but arktype schemas are functions. The check silently misidentified the second schema as a default value in multi-version definitions.Remove KV migration machinery
Removes the variadic
defineKv(v1, v2, ...).migrate(fn, default)pattern and all migration infrastructure from KV stores.defineKvnow has exactly one signature:defineKv(schema, defaultValue).The variadic pattern had zero production usage across 44
defineKvcall sites in Whispering—every single one already used the shorthand. The multi-version overload existed only for test coverage, and theisSecondArgSchemadetection heuristic it relied on caused a real bug: arktype schemas are functions, not objects, sotypeof args[1] === 'object'silently misidentified the second schema as a default value. More fundamentally, KV stores hold scalar preferences, not accumulated rows. Schema changes either don't break validation (widening an enum) or the default fallback is acceptable (resetting a toggle). Migration is structurally unnecessary for this data lifecycle.The type system reflects the simplification.
KvDefinitiondropped itsTVersionstuple generic for a singleTSchema, and themigratefield is gone:InferKvValuesimplified fromLastSchemaindirection to direct extraction.InferKvVersionUnionwas removed entirely.LastSchemaandcreateUnionSchemaremain untouched—tables still use them.In
create-kv.ts, theparseValuehelper (which existed solely to calldefinition.migrate) was removed.get(),observe(), andobserveAll()now returnresult.valuedirectly after validation. The file went from importingCombinedStandardSchemaandKvGetResult(for the helper's type signature) to importing only what it needs.define-kv.tswent from 163 lines (two overloads, implementation withisSecondArgSchemabranching,createUnionSchemaimport) to 47 lines (one function that returns{ schema, defaultValue }).Six documentation files were updated to replace variadic KV examples with the shorthand pattern. The workspace-api skill file now documents a scalar-per-key convention: use dot-namespaced keys (
'theme.mode','theme.fontSize') instead of structured objects, which makes migration structurally unnecessary. A seventh doc (inline-single-use-definitions-in-tests.md) was caught in a straggler sweep—itsdefineKvexamples were missing the now-required default value.All 347 tests pass. Zero production code changes—every Whispering
defineKvcall was already using the shorthand.Schema polish
The Whispering workspace definition had structural issues from rapid prototyping: a
transcription.configblob KV that should have been individual keys,transformationStepsusing a nested discriminated union that could be a flat camelCase schema, and no JSDoc on any table or KV group.Also added
completion.openrouter.modelKV entry and used discriminated unions for transformation run schemas (spec Decision 4).Why remove instead of deprecate?
Zero production callers. The variadic pattern had test-only usage and had already caused one bug. Deprecation warnings serve callers who need time to migrate; there were none.
Why not add an optional
migratecallback?No production KV entry needs one today. If a genuine case appears, the escape hatch is straightforward: add a new KV key with the new schema and write app-level migration code. Or add
{ migrate? }as an optional third parameter at that time.Tables are deliberately different
Tables accumulate rows that must survive schema changes—migration is mandatory. KV stores hold single preferences where resetting to default is acceptable. This PR makes the asymmetry explicit rather than having KV borrow table machinery it doesn't need.