Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
10cd822
refactor(workspace): replace transformationSteps discriminated union …
braden-w Mar 12, 2026
c247df1
refactor(workspace): break transcription.config blob into individual KVs
braden-w Mar 12, 2026
26fea9a
feat(workspace): add completion.openrouter.model KV entry
braden-w Mar 12, 2026
1c69815
docs(workspace): add JSDoc to every table and KV group
braden-w Mar 12, 2026
2a1125d
docs(spec): mark Wave 1 complete with audit findings
braden-w Mar 12, 2026
a53fa2a
docs(spec): add architecture diagrams and wave progression overview
braden-w Mar 13, 2026
a05ea77
refactor(workspace): use discriminated unions for transformation run …
braden-w Mar 13, 2026
773d3d7
docs(spec): add Decision 4 — discriminated unions for run tables
braden-w Mar 13, 2026
c8e64f7
feat(workspace): add defaultValue to KvDefinition and simplify get() …
braden-w Mar 13, 2026
d3e8b38
feat(workspace): simplify get() to return default and add observeAll()
braden-w Mar 13, 2026
b017b78
feat(workspace): add defaults to all defineKv() calls and fix multi-v…
braden-w Mar 13, 2026
d11babe
docs(spec): note variadic defineKv pattern is likely removable
braden-w Mar 13, 2026
9eb93d6
Merge remote-tracking branch 'origin/main' into opencode/silent-squid
braden-w Mar 13, 2026
b5ce9a3
refactor(workspace): remove KV migration machinery from types, define…
braden-w Mar 13, 2026
fd552cf
docs(workspace): update documentation to reflect simplified KV API
braden-w Mar 13, 2026
3345907
docs(spec): mark remove-kv-migration spec as Implemented with review …
braden-w Mar 13, 2026
b35ed72
docs: fix stale defineKv examples missing default value
braden-w Mar 13, 2026
54fad99
docs(workspace): add KV philosophy JSDoc and remove dead KvGetResult …
braden-w Mar 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .agents/skills/typescript/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ When a schema, builder, or configuration is only used once in a test, inline it
test('creates workspace with tables', () => {
const posts = defineTable(type({ id: 'string', title: 'string', _v: '1' }));

const theme = defineKv(type({ mode: "'light' | 'dark'", _v: '1' }));
const theme = defineKv(type("'light' | 'dark'"), 'light');

const workspace = defineWorkspace({
id: 'test-app',
Expand All @@ -480,7 +480,7 @@ test('creates workspace with tables', () => {
posts: defineTable(type({ id: 'string', title: 'string', _v: '1' })),
},
kv: {
theme: defineKv(type({ mode: "'light' | 'dark'", _v: '1' })),
theme: defineKv(type("'light' | 'dark'"), 'light'),
},
});

Expand Down
70 changes: 30 additions & 40 deletions .agents/skills/workspace-api/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,20 @@
---
name: workspace-api
description: Workspace API patterns for defineTable, defineKv, versioning, and migrations. Use when defining workspace schemas, adding versions to existing tables/KV stores, or writing migration functions.
description: Workspace API patterns for defineTable, defineKv, versioning, and migrations. Use when defining workspace schemas, adding versions to existing tables, or writing migration functions.
metadata:
author: epicenter
version: '4.0'
---

# Workspace API

Type-safe schema definitions for tables and KV stores with versioned migrations.
Type-safe schema definitions for tables and KV stores.

## When to Apply This Skill

- Defining a new table or KV store with `defineTable()` or `defineKv()`
- Adding a new version to an existing definition
- Writing migration functions
- Converting from shorthand to builder pattern
- Adding a new version to an existing table definition
- Writing table migration functions

## Tables

Expand Down Expand Up @@ -53,43 +52,34 @@ const posts = defineTable()

## KV Stores

KV stores are flexible — `_v` is optional. Both patterns work:

### Without `_v` (field presence)
KV stores use `defineKv(schema, defaultValue)`. No versioning, no migration—invalid stored data falls back to the default.

```typescript
import { defineKv } from '@epicenter/workspace';
import { type } from 'arktype';

const sidebar = defineKv(type({ collapsed: 'boolean', width: 'number' }));

// Multi-version with field presence
const theme = defineKv()
.version(type({ mode: "'light' | 'dark'" }))
.version(type({ mode: "'light' | 'dark' | 'system'", fontSize: 'number' }))
.migrate((v) => {
if (!('fontSize' in v)) return { ...v, fontSize: 14 };
return v;
});
const sidebar = defineKv(type({ collapsed: 'boolean', width: 'number' }), { collapsed: false, width: 300 });
const fontSize = defineKv(type('number'), 14);
const enabled = defineKv(type('boolean'), true);
```

### With `_v` (explicit discriminant)
### KV Design Convention: One Scalar Per Key

Use dot-namespaced keys for logical groupings of scalar values:

```typescript
const theme = defineKv()
.version(type({ mode: "'light' | 'dark'", _v: '1' }))
.version(
type({ mode: "'light' | 'dark' | 'system'", fontSize: 'number', _v: '2' }),
)
.migrate((v) => {
switch (v._v) {
case 1:
return { ...v, fontSize: 14, _v: 2 };
case 2:
return v;
}
});
// ✅ Correct — each preference is an independent scalar
'theme.mode': defineKv(type("'light' | 'dark' | 'system'"), 'light'),
'theme.fontSize': defineKv(type('number'), 14),

// ❌ Wrong — structured object invites migration needs
'theme': defineKv(type({ mode: "'light' | 'dark'", fontSize: 'number' }), { mode: 'light', fontSize: 14 }),
```

With scalar values, schema changes either don't break validation (widening `'light' | 'dark'` to `'light' | 'dark' | 'system'` still validates old data) or the default fallback is acceptable (resetting a toggle takes one click).

Exception: discriminated unions and `Record<string, T> | null` are acceptable when they represent a single atomic value.

## Branded Table IDs (Required)

Every table's `id` field and every string foreign key field MUST use a branded type instead of plain `'string'`. This prevents accidental mixing of IDs from different tables at compile time.
Expand Down Expand Up @@ -238,20 +228,20 @@ export const workspaceClient = createWorkspace(

- `_v` is a **number** discriminant field (`'1'` in arktype = the literal number `1`)
- **Required for tables** — enforced at the type level via `CombinedStandardSchema<{ id: string; _v: number }>`
- **Optional for KV stores** — KV keeps full flexibility
- **Not used by KV stores** — KV has no versioning; `defineKv(schema, defaultValue)` is the only pattern
- In arktype schemas: `_v: '1'`, `_v: '2'`, `_v: '3'` (number literals)
- In migration returns: `_v: 2` (TypeScript narrows automatically, `as const` is unnecessary)
- Convention: `_v` goes last in the object (`{ id, ...fields, _v: '1' }`)

## Migration Function Rules
## Table Migration Function Rules

1. Input type is a union of all version outputs
2. Return type is the latest version output
3. Use `switch (row._v)` for discrimination (tables always have `_v`)
4. Final case returns `row` as-is (already latest)
5. Always migrate directly to latest (not incrementally through each version)

## Anti-Patterns
## Table Anti-Patterns

### Incremental migration (v1 -> v2 -> v3)

Expand Down Expand Up @@ -285,8 +275,8 @@ return { ...row, views: 0, _v: 2 as const }; // Also works — redundant

## References

- `packages/epicenter/src/workspace/define-table.ts`
- `packages/epicenter/src/workspace/define-kv.ts`
- `packages/epicenter/src/workspace/index.ts`
- `packages/epicenter/src/workspace/create-tables.ts`
- `packages/epicenter/src/workspace/create-kv.ts`
- `packages/workspace/src/workspace/define-table.ts`
- `packages/workspace/src/workspace/define-kv.ts`
- `packages/workspace/src/workspace/index.ts`
- `packages/workspace/src/workspace/create-tables.ts`
- `packages/workspace/src/workspace/create-kv.ts`
Loading
Loading