From 2c0c7efc8dfd2da04d495d4c741b9cac066a89c0 Mon Sep 17 00:00:00 2001 From: bradleyshep <148254416+bradleyshep@users.noreply.github.com> Date: Wed, 24 Jun 2026 09:04:50 -0400 Subject: [PATCH] Single SDK reference: always use the official customer skills (typescript-server/client SKILL.md). Remove focused/fork alternate refs and STDB_SDK_REF switch --- tools/llm-sequential-upgrade/DEVELOP.md | 2 +- .../backends/spacetime-sdk-focused.md | 164 ----------- .../backends/spacetime-sdk-rules.md | 258 ------------------ tools/llm-sequential-upgrade/run.sh | 31 +-- 4 files changed, 13 insertions(+), 442 deletions(-) delete mode 100644 tools/llm-sequential-upgrade/backends/spacetime-sdk-focused.md delete mode 100644 tools/llm-sequential-upgrade/backends/spacetime-sdk-rules.md diff --git a/tools/llm-sequential-upgrade/DEVELOP.md b/tools/llm-sequential-upgrade/DEVELOP.md index e9e19db970..18251388bb 100644 --- a/tools/llm-sequential-upgrade/DEVELOP.md +++ b/tools/llm-sequential-upgrade/DEVELOP.md @@ -269,8 +269,8 @@ llm-sequential-upgrade/ parse-telemetry.mjs # Telemetry → COST_REPORT.md backends/ spacetime.md # SpacetimeDB-specific phases - spacetime-sdk-rules.md # SpacetimeDB SDK patterns spacetime-templates.md # Code templates + # SDK reference = the official skills/typescript-{server,client}/SKILL.md postgres.md # PostgreSQL-specific phases mongodb.md # MongoDB-specific phases test-plans/ diff --git a/tools/llm-sequential-upgrade/backends/spacetime-sdk-focused.md b/tools/llm-sequential-upgrade/backends/spacetime-sdk-focused.md deleted file mode 100644 index fc0402bee4..0000000000 --- a/tools/llm-sequential-upgrade/backends/spacetime-sdk-focused.md +++ /dev/null @@ -1,164 +0,0 @@ -# SpacetimeDB TypeScript SDK Reference (focused) - -Lean, chat-app-focused SDK reference for the SpacetimeDB **2.x** TypeScript SDK — -parity in scope/prescriptiveness with the PostgreSQL/MongoDB backend files. Covers only -what this app needs; omits SDK features the app doesn't use. - -## Imports - -```typescript -import { schema, table, t } from 'spacetimedb/server'; -import { SenderError } from 'spacetimedb/server'; -import { ScheduleAt } from 'spacetimedb'; // scheduled tables only -``` - -## Tables - -`table(OPTIONS, COLUMNS)` — two arguments. `name` is snake_case. - -```typescript -const user = table( - { name: 'user', public: true }, - { identity: t.identity().primaryKey(), name: t.string(), online: t.bool() } -); -``` - -Options: `name` (snake_case), `public: true`, `scheduled: (): any => reducerRef`, `indexes: [...]`. -`ctx.db` accessors use the **camelCase** form of the table's `name`. - -## Column Types - -`t.u64()`/`t.i64()` → bigint (use `0n` literals) · `t.u32()`/`t.i32()`/`t.f64()` → number · -`t.bool()` · `t.string()` · `t.identity()` → Identity · `t.timestamp()` → Timestamp · -`t.scheduleAt()` → ScheduleAt · optional: `t.option(t.string())` - -Modifiers: `.primaryKey()` `.autoInc()` `.unique()` `.index('btree')` - -## Indexes - -```typescript -// single-column inline (preferred): -authorId: t.u64().index('btree'), // → ctx.db.post.authorId.filter(authorId) -// multi-column (named): -indexes: [{ accessor: 'by_room_user', algorithm: 'btree', columns: ['roomId', 'userIdentity'] }] -// → ctx.db.draft.by_room_user.filter([roomId, identity]) -``` - -## Schema Export - -```typescript -const spacetimedb = schema({ user, room, message }); // ONE object, not spread args -export default spacetimedb; -``` - -## Reducers - -Export name becomes the reducer name. - -```typescript -export const sendMessage = spacetimedb.reducer( - { roomId: t.u64(), text: t.string() }, - (ctx, { roomId, text }) => { - ctx.db.message.insert({ id: 0n, roomId, sender: ctx.sender, text, sentAt: ctx.timestamp }); - } -); -// no arguments — just the callback: -export const reset = spacetimedb.reducer((ctx) => { ... }); -``` - -## DB Operations - -```typescript -ctx.db.message.insert({ id: 0n, ... }); // insert (0n for autoInc PK) -ctx.db.message.id.find(msgId); // by PK → row | null -ctx.db.user.identity.find(ctx.sender); // by unique column -[...ctx.db.message.roomId.filter(roomId)]; // filter → spread to Array -[...ctx.db.message.iter()]; // all rows → Array -ctx.db.message.id.update({ ...existing, text }); // update (spread + override) -ctx.db.message.id.delete(msgId); -``` - -`iter()`/`filter()` return iterators — spread to Array for `.sort()`/`.map()`/`.filter()`. - -## Lifecycle Hooks - -MUST be `export const` (bare calls are silently ignored). - -```typescript -export const init = spacetimedb.init((ctx) => { ... }); -export const onConnect = spacetimedb.clientConnected((ctx) => { ... }); -export const onDisconnect = spacetimedb.clientDisconnected((ctx) => { ... }); -``` - -## Reducer Context — identity, time, randomness - -Inside a reducer, get sender / time / randomness **only** from `ctx`. -**Standard-library clocks and random sources (`Date.now()`, `Math.random()`) are NOT available -in modules** — use `ctx` instead. - -```typescript -ctx.sender // caller Identity -if (!row.owner.equals(ctx.sender)) throw new SenderError('unauthorized'); -ctx.timestamp // deterministic server time -ctx.db.message.insert({ ..., createdAt: ctx.timestamp }); -ctx.random(); // [0.0, 1.0) -ctx.random.integerInRange(1, 6); // inclusive -// Client: Timestamp → Date -new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n)); -``` - -## Scheduled Tables (timers) - -```typescript -const tickTimer = table( - { name: 'tick_timer', scheduled: (): any => tick }, // (): any => breaks circular dep - { scheduledId: t.u64().primaryKey().autoInc(), scheduledAt: t.scheduleAt() } -); -export const tick = spacetimedb.reducer( - { timer: tickTimer.rowType }, - (ctx, { timer }) => { /* timer row auto-deleted after this runs */ } -); -// one-shot: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + delayMicros) -// repeating: ScheduleAt.interval(60_000_000n) -``` - -## React Client — main.tsx - -```typescript -import { SpacetimeDBProvider } from 'spacetimedb/react'; -import { DbConnection } from './module_bindings'; -import { MODULE_NAME, SPACETIMEDB_URI } from './config'; - -const connectionBuilder = useMemo(() => - DbConnection.builder() - .withUri(SPACETIMEDB_URI) - .withDatabaseName(MODULE_NAME) - .withToken(localStorage.getItem('auth_token') || undefined), - []); -// -``` - -## React Client — App.tsx - -```typescript -import { useTable, useSpacetimeDB } from 'spacetimedb/react'; -import { DbConnection, tables } from './module_bindings'; - -const { isActive, identity: myIdentity, token, getConnection } = useSpacetimeDB(); -const conn = getConnection() as DbConnection | null; - -useEffect(() => { if (token) localStorage.setItem('auth_token', token); }, [token]); - -useEffect(() => { // subscribe once connected - if (!conn || !isActive) return; - conn.subscriptionBuilder() - .onApplied(() => setReady(true)) - .subscribe([tables.user, tables.message]); // typed tables (raw SQL strings also accepted) -}, [conn, isActive]); - -const [users] = useTable(tables.user); // reactive rows; returns [rows, isReady] -const [messages] = useTable(tables.message); - -conn?.reducers.sendMessage({ roomId, text }); // call reducers with object args -const isMe = row.owner.toHexString() === myIdentity?.toHexString(); -``` diff --git a/tools/llm-sequential-upgrade/backends/spacetime-sdk-rules.md b/tools/llm-sequential-upgrade/backends/spacetime-sdk-rules.md deleted file mode 100644 index 337af9269a..0000000000 --- a/tools/llm-sequential-upgrade/backends/spacetime-sdk-rules.md +++ /dev/null @@ -1,258 +0,0 @@ -# SpacetimeDB TypeScript SDK Reference - -## Imports - -```typescript -import { schema, table, t } from 'spacetimedb/server'; -import { SenderError } from 'spacetimedb/server'; -import { ScheduleAt } from 'spacetimedb'; // for scheduled tables only -``` - -## Tables - -`table(OPTIONS, COLUMNS)` — two arguments. The `name` field MUST be snake_case: - -```typescript -const entity = table( - { name: 'entity', public: true }, - { - identity: t.identity().primaryKey(), - name: t.string(), - active: t.bool(), - } -); -``` - -Options: `name` (snake_case, required), `public: true`, `event: true`, `scheduled: (): any => reducerRef`, `indexes: [...]` - -`ctx.db` accessors use the JS variable name (camelCase), not the SQL name. - -## Column Types - -| Builder | JS type | Notes | -|---------|---------|-------| -| `t.u64()` | bigint | Use `0n` literals | -| `t.i64()` | bigint | Use `0n` literals | -| `t.u32()` / `t.i32()` | number | | -| `t.f64()` / `t.f32()` | number | | -| `t.bool()` | boolean | | -| `t.string()` | string | | -| `t.identity()` | Identity | | -| `t.timestamp()` | Timestamp | | -| `t.scheduleAt()` | ScheduleAt | | - -Modifiers: `.primaryKey()`, `.autoInc()`, `.unique()`, `.index('btree')` - -Optional columns: `nickname: t.option(t.string())` - -## Indexes - -Prefer inline `.index('btree')` for single-column. Use named indexes only for multi-column: - -```typescript -// Inline (preferred): -authorId: t.u64().index('btree'), -// Access: ctx.db.post.authorId.filter(authorId); - -// Multi-column (named): -indexes: [{ accessor: 'by_cat_sev', algorithm: 'btree', columns: ['category', 'severity'] }] -``` - -## Schema Export - -```typescript -const spacetimedb = schema({ entity, record }); // ONE object, not spread args -export default spacetimedb; -``` - -## Reducers - -Export name becomes the reducer name: - -```typescript -export const createEntity = spacetimedb.reducer( - { name: t.string(), age: t.i32() }, - (ctx, { name, age }) => { - ctx.db.entity.insert({ identity: ctx.sender, name, age, active: true }); - } -); - -// No arguments — just the callback: -export const doReset = spacetimedb.reducer((ctx) => { ... }); -``` - -## DB Operations - -```typescript -ctx.db.entity.insert({ id: 0n, name: 'Sample' }); // Insert (0n for autoInc) -ctx.db.entity.id.find(entityId); // Find by PK → row | null -ctx.db.entity.identity.find(ctx.sender); // Find by unique column -[...ctx.db.item.authorId.filter(authorId)]; // Filter → spread to Array -[...ctx.db.entity.iter()]; // All rows → Array -ctx.db.entity.id.update({ ...existing, name: newName }); // Update (spread + override) -ctx.db.entity.id.delete(entityId); // Delete by PK -``` - -Note: `iter()` and `filter()` return iterators. Spread to Array for `.sort()`, `.filter()`, `.map()`. - -## Lifecycle Hooks - -MUST be `export const` — bare calls are silently ignored: - -```typescript -export const init = spacetimedb.init((ctx) => { ... }); -export const onConnect = spacetimedb.clientConnected((ctx) => { ... }); -export const onDisconnect = spacetimedb.clientDisconnected((ctx) => { ... }); -``` - -## Authentication & Timestamps - -```typescript -// Auth: ctx.sender is the caller's Identity -if (!row.owner.equals(ctx.sender)) throw new SenderError('unauthorized'); - -// Server timestamps -ctx.db.item.insert({ id: 0n, createdAt: ctx.timestamp }); - -// Client: Timestamp → Date -new Date(Number(row.createdAt.microsSinceUnixEpoch / 1000n)); -``` - -## Scheduled Tables - -```typescript -const tickTimer = table({ - name: 'tick_timer', - scheduled: (): any => tick, // (): any => breaks circular dep -}, { - scheduledId: t.u64().primaryKey().autoInc(), - scheduledAt: t.scheduleAt(), -}); - -export const tick = spacetimedb.reducer( - { timer: tickTimer.rowType }, - (ctx, { timer }) => { /* timer row auto-deleted after this runs */ } -); - -// One-time: ScheduleAt.time(ctx.timestamp.microsSinceUnixEpoch + delayMicros) -// Repeating: ScheduleAt.interval(60_000_000n) -``` - -## React Client - -### main.tsx — SpacetimeDBProvider is required - -```typescript -import React, { useMemo } from 'react'; -import ReactDOM from 'react-dom/client'; -import { SpacetimeDBProvider } from 'spacetimedb/react'; -import { DbConnection } from './module_bindings'; -import { MODULE_NAME, SPACETIMEDB_URI } from './config'; -import App from './App'; - -function Root() { - const connectionBuilder = useMemo(() => - DbConnection.builder() - .withUri(SPACETIMEDB_URI) - .withDatabaseName(MODULE_NAME) - .withToken(localStorage.getItem('auth_token') || undefined), - [] - ); - return ( - - - - ); -} - -ReactDOM.createRoot(document.getElementById('root')!).render(); -``` - -### App.tsx patterns - -```typescript -import { useTable, useSpacetimeDB } from 'spacetimedb/react'; -import { DbConnection, tables } from './module_bindings'; - -function App() { - const { isActive, identity: myIdentity, token, getConnection } = useSpacetimeDB(); - const conn = getConnection() as DbConnection | null; - - // Save auth token - useEffect(() => { if (token) localStorage.setItem('auth_token', token); }, [token]); - - // Subscribe when connected - useEffect(() => { - if (!conn || !isActive) return; - conn.subscriptionBuilder() - .onApplied(() => setSubscribed(true)) - .subscribe(['SELECT * FROM entity', 'SELECT * FROM record']); - }, [conn, isActive]); - - // Reactive data - const [entities] = useTable(tables.entity); - const [records] = useTable(tables.record); - - // Call reducers with object syntax - conn?.reducers.addRecord({ data }); - - // Compare identities - const isMe = row.owner.toHexString() === myIdentity?.toHexString(); -} -``` - -## Complete Example - -```typescript -// schema.ts -import { schema, table, t } from 'spacetimedb/server'; - -const entity = table({ name: 'entity', public: true }, { - identity: t.identity().primaryKey(), - name: t.string(), - active: t.bool(), -}); - -const record = table({ name: 'record', public: true }, { - id: t.u64().primaryKey().autoInc(), - owner: t.identity(), - value: t.u32(), - createdAt: t.timestamp(), -}); - -const spacetimedb = schema({ entity, record }); -export default spacetimedb; -``` - -```typescript -// index.ts -import spacetimedb from './schema'; -import { t, SenderError } from 'spacetimedb/server'; -export { default } from './schema'; - -export const onConnect = spacetimedb.clientConnected((ctx) => { - const existing = ctx.db.entity.identity.find(ctx.sender); - if (existing) ctx.db.entity.identity.update({ ...existing, active: true }); -}); - -export const onDisconnect = spacetimedb.clientDisconnected((ctx) => { - const existing = ctx.db.entity.identity.find(ctx.sender); - if (existing) ctx.db.entity.identity.update({ ...existing, active: false }); -}); - -export const createEntity = spacetimedb.reducer( - { name: t.string() }, - (ctx, { name }) => { - if (ctx.db.entity.identity.find(ctx.sender)) throw new SenderError('already exists'); - ctx.db.entity.insert({ identity: ctx.sender, name, active: true }); - } -); - -export const addRecord = spacetimedb.reducer( - { value: t.u32() }, - (ctx, { value }) => { - if (!ctx.db.entity.identity.find(ctx.sender)) throw new SenderError('not found'); - ctx.db.record.insert({ id: 0n, owner: ctx.sender, value, createdAt: ctx.timestamp }); - } -); -``` diff --git a/tools/llm-sequential-upgrade/run.sh b/tools/llm-sequential-upgrade/run.sh index b05e878652..349f4fc7ba 100644 --- a/tools/llm-sequential-upgrade/run.sh +++ b/tools/llm-sequential-upgrade/run.sh @@ -481,7 +481,8 @@ snapshot_inputs() { # Backend specs (only relevant backend) cp "$SCRIPT_DIR/backends/$BACKEND.md" "$INPUTS_DIR/backends/" 2>/dev/null || true if [[ "$BACKEND" == "spacetime" ]]; then - cp "$SCRIPT_DIR/backends/spacetime-sdk-rules.md" "$INPUTS_DIR/backends/" 2>/dev/null || true + cp "$SCRIPT_DIR/../../skills/typescript-server/SKILL.md" "$INPUTS_DIR/backends/typescript-server-SKILL.md" 2>/dev/null || true + cp "$SCRIPT_DIR/../../skills/typescript-client/SKILL.md" "$INPUTS_DIR/backends/typescript-client-SKILL.md" 2>/dev/null || true cp "$SCRIPT_DIR/backends/spacetime-templates.md" "$INPUTS_DIR/backends/" 2>/dev/null || true fi @@ -750,7 +751,11 @@ if [[ -z "$FIX_MODE" && -z "$UPGRADE_MODE" ]]; then elif [[ "$RULES" == "standard" ]]; then case "$BACKEND" in spacetime) - cat "$SCRIPT_DIR/backends/spacetime-sdk-rules.md" > "$APP_DIR/CLAUDE.md" + _strip='NR==1 && /^---$/ {fm=1; next} fm && /^---$/ {fm=0; next} !fm {print}' + { awk "$_strip" "$SCRIPT_DIR/../../skills/typescript-server/SKILL.md" + echo ""; echo "---"; echo "" + awk "$_strip" "$SCRIPT_DIR/../../skills/typescript-client/SKILL.md" + } > "$APP_DIR/CLAUDE.md" ;; mongodb) echo "# MongoDB Backend" > "$APP_DIR/CLAUDE.md" @@ -771,31 +776,19 @@ if [[ -z "$FIX_MODE" && -z "$UPGRADE_MODE" ]]; then esac echo "Assembled standard CLAUDE.md (rules=$RULES)" else - # guided (default) — phases + SDK reference + templates. - # SDK reference is selectable via STDB_SDK_REF: focused (default) | skills | fork + # guided (default) — phases + the official customer SDK skills + templates. if [[ "$BACKEND" == "spacetime" ]]; then _strip_fm() { awk 'NR==1 && /^---$/ {fm=1; next} fm && /^---$/ {fm=0; next} !fm {print}' "$1"; } - _sdk_ref="${STDB_SDK_REF:-focused}" { cat "$SCRIPT_DIR/backends/spacetime.md" echo ""; echo "---"; echo "" - case "$_sdk_ref" in - skills) - _strip_fm "$SCRIPT_DIR/../../skills/typescript-server/SKILL.md" - echo ""; echo "---"; echo "" - _strip_fm "$SCRIPT_DIR/../../skills/typescript-client/SKILL.md" - ;; - fork) - cat "$SCRIPT_DIR/backends/spacetime-sdk-rules.md" - ;; - *) # focused (default) — lean reference, parity in scope with mongo/pg backend files - cat "$SCRIPT_DIR/backends/spacetime-sdk-focused.md" - ;; - esac + _strip_fm "$SCRIPT_DIR/../../skills/typescript-server/SKILL.md" + echo ""; echo "---"; echo "" + _strip_fm "$SCRIPT_DIR/../../skills/typescript-client/SKILL.md" echo ""; echo "---"; echo "" cat "$SCRIPT_DIR/backends/spacetime-templates.md" } > "$APP_DIR/CLAUDE.md" - echo "Assembled guided CLAUDE.md from spacetime.md + SDK ref [$_sdk_ref] + templates" + echo "Assembled guided CLAUDE.md from spacetime.md + official skills + templates" else cp "$SCRIPT_DIR/backends/$BACKEND.md" "$APP_DIR/CLAUDE.md" echo "Copied backends/$BACKEND.md → app CLAUDE.md"