P3-T00: extended node fields — surface_hint, workspace, restore_policy, package_refs

Migration 007_node_spec_fields adds four columns to nodes table for
RigSpec portability:
- surface_hint: layout placement hint (e.g. "tab:workers")
- workspace: workspace assignment
- restore_policy: per-node restore behavior
- package_refs: JSON array of package references

Node type widened with explicit defaults: surfaceHint/workspace/restorePolicy
= null, packageRefs = [] (never undefined).

RigRepository addNode persists extended fields. rowToNode parses
package_refs JSON to string[].

All boot paths wired: startup.ts + test-app.ts include 007.
All test files with own migration lists updated.

10 new tests (5 schema + 4 domain + 1 wiring), 366 total, all passing.
QA-approved HIGH risk.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
Mike SchwarzandClaude Opus 4.6 committed 2026-03-23 23:53:21 -07:00
1 parent 1214f93a1e
commit 3208892d08
14 files changed
+172 -12

No files matched your search

@@ -0,0 +1,12 @@
import type { Migration } from "../migrate.js";
export const nodeSpecFieldsSchema: Migration = {
name: "007_node_spec_fields.sql",
sql: `
-- Extended node fields for RigSpec portability (Phase 3)
ALTER TABLE nodes ADD COLUMN surface_hint TEXT;
ALTER TABLE nodes ADD COLUMN workspace TEXT;
ALTER TABLE nodes ADD COLUMN restore_policy TEXT;
ALTER TABLE nodes ADD COLUMN package_refs TEXT; -- JSON array of strings
`,
};
+18 -2
View File
@@ -14,6 +14,10 @@ interface NodeOptions {
runtime?: string;
model?: string;
cwd?: string;
surfaceHint?: string;
workspace?: string;
restorePolicy?: string;
packageRefs?: string[];
}
export class RigRepository {
@@ -37,7 +41,7 @@ export class RigRepository {
const id = ulid();
this.db
.prepare(
"INSERT INTO nodes (id, rig_id, logical_id, role, runtime, model, cwd) VALUES (?, ?, ?, ?, ?, ?, ?)"
"INSERT INTO nodes (id, rig_id, logical_id, role, runtime, model, cwd, surface_hint, workspace, restore_policy, package_refs) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
)
.run(
id,
@@ -46,7 +50,11 @@ export class RigRepository {
opts?.role ?? null,
opts?.runtime ?? null,
opts?.model ?? null,
opts?.cwd ?? null
opts?.cwd ?? null,
opts?.surfaceHint ?? null,
opts?.workspace ?? null,
opts?.restorePolicy ?? null,
opts?.packageRefs ? JSON.stringify(opts.packageRefs) : null
);
return this.rowToNode(
@@ -131,6 +139,10 @@ export class RigRepository {
runtime: row.runtime,
model: row.model,
cwd: row.cwd,
surfaceHint: row.surface_hint ?? null,
workspace: row.workspace ?? null,
restorePolicy: row.restore_policy ?? null,
packageRefs: row.package_refs ? JSON.parse(row.package_refs) as string[] : [],
createdAt: row.created_at,
};
}
@@ -177,6 +189,10 @@ interface NodeRow {
runtime: string | null;
model: string | null;
cwd: string | null;
surface_hint: string | null;
workspace: string | null;
restore_policy: string | null;
package_refs: string | null;
created_at: string;
}
+4
View File
@@ -13,6 +13,10 @@ export interface Node {
runtime: string | null;
model: string | null;
cwd: string | null;
surfaceHint: string | null;
workspace: string | null;
restorePolicy: string | null;
packageRefs: string[];
createdAt: string;
}
+2 -1
View File
@@ -26,6 +26,7 @@ import { createApp, type AppDeps } from "./server.js";
import { snapshotsSchema } from "./db/migrations/004_snapshots.js";
import { checkpointsSchema } from "./db/migrations/005_checkpoints.js";
import { resumeMetadataSchema } from "./db/migrations/006_resume_metadata.js";
import { nodeSpecFieldsSchema } from "./db/migrations/007_node_spec_fields.js";
interface DaemonOptions {
dbPath?: string;
@@ -44,7 +45,7 @@ interface DaemonResult {
export async function createDaemon(opts?: DaemonOptions): Promise<DaemonResult> {
const dbPath = opts?.dbPath ?? ":memory:";
const db = createDb(dbPath);
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema]);
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema, nodeSpecFieldsSchema]);
const rigRepo = new RigRepository(db);
const sessionRegistry = new SessionRegistry(db);
@@ -4,11 +4,12 @@ import { createDb } from "../src/db/connection.js";
import { migrate } from "../src/db/migrate.js";
import { coreSchema } from "../src/db/migrations/001_core_schema.js";
import { checkpointsSchema } from "../src/db/migrations/005_checkpoints.js";
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
import { CheckpointStore } from "../src/domain/checkpoint-store.js";
function setupDb(): Database.Database {
const db = createDb();
migrate(db, [coreSchema, checkpointsSchema]);
migrate(db, [coreSchema, checkpointsSchema, nodeSpecFieldsSchema]);
return db;
}
+2 -1
View File
@@ -8,6 +8,7 @@ import { eventsSchema } from "../../src/db/migrations/003_events.js";
import { snapshotsSchema } from "../../src/db/migrations/004_snapshots.js";
import { checkpointsSchema } from "../../src/db/migrations/005_checkpoints.js";
import { resumeMetadataSchema } from "../../src/db/migrations/006_resume_metadata.js";
import { nodeSpecFieldsSchema } from "../../src/db/migrations/007_node_spec_fields.js";
import { RigRepository } from "../../src/domain/rig-repository.js";
import { SessionRegistry } from "../../src/domain/session-registry.js";
import { EventBus } from "../../src/domain/event-bus.js";
@@ -25,7 +26,7 @@ import { createApp } from "../../src/server.js";
export function createFullTestDb(): Database.Database {
const db = createDb();
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema]);
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema, nodeSpecFieldsSchema]);
return db;
}
+2 -1
View File
@@ -5,6 +5,7 @@ import { migrate } from "../src/db/migrate.js";
import { coreSchema } from "../src/db/migrations/001_core_schema.js";
import { bindingsSessionsSchema } from "../src/db/migrations/002_bindings_sessions.js";
import { eventsSchema } from "../src/db/migrations/003_events.js";
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
import { RigRepository } from "../src/domain/rig-repository.js";
import { SessionRegistry } from "../src/domain/session-registry.js";
import { EventBus } from "../src/domain/event-bus.js";
@@ -14,7 +15,7 @@ import type { PersistedEvent } from "../src/domain/types.js";
function setupDb(): Database.Database {
const db = createDb();
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema]);
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, nodeSpecFieldsSchema]);
return db;
}
+2 -1
View File
@@ -5,6 +5,7 @@ import { migrate } from "../src/db/migrate.js";
import { coreSchema } from "../src/db/migrations/001_core_schema.js";
import { bindingsSessionsSchema } from "../src/db/migrations/002_bindings_sessions.js";
import { eventsSchema } from "../src/db/migrations/003_events.js";
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
import { RigRepository } from "../src/domain/rig-repository.js";
import { SessionRegistry } from "../src/domain/session-registry.js";
import { EventBus } from "../src/domain/event-bus.js";
@@ -14,7 +15,7 @@ import type { PersistedEvent } from "../src/domain/types.js";
function setupDb(): Database.Database {
const db = createDb();
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema]);
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, nodeSpecFieldsSchema]);
return db;
}
@@ -11,6 +11,7 @@ import { eventsSchema } from "../src/db/migrations/003_events.js";
import { snapshotsSchema } from "../src/db/migrations/004_snapshots.js";
import { checkpointsSchema } from "../src/db/migrations/005_checkpoints.js";
import { resumeMetadataSchema } from "../src/db/migrations/006_resume_metadata.js";
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
import { RigRepository } from "../src/domain/rig-repository.js";
import { SessionRegistry } from "../src/domain/session-registry.js";
import { EventBus } from "../src/domain/event-bus.js";
@@ -27,7 +28,7 @@ import type { PersistedEvent, Snapshot } from "../src/domain/types.js";
function setupDb(): Database.Database {
const db = createDb();
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema]);
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema, nodeSpecFieldsSchema]);
return db;
}
+59 -1
View File
@@ -4,11 +4,12 @@ import { createDb } from "../src/db/connection.js";
import { migrate } from "../src/db/migrate.js";
import { coreSchema } from "../src/db/migrations/001_core_schema.js";
import { bindingsSessionsSchema } from "../src/db/migrations/002_bindings_sessions.js";
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
import { RigRepository } from "../src/domain/rig-repository.js";
function setupDb(): Database.Database {
const db = createDb();
migrate(db, [coreSchema, bindingsSessionsSchema]);
migrate(db, [coreSchema, bindingsSessionsSchema, nodeSpecFieldsSchema]);
return db;
}
@@ -138,4 +139,61 @@ describe("RigRepository", () => {
expect(repo.getRig(rig.id)).toBeNull();
expect(repo.listRigs()).toHaveLength(0);
});
// -- P3-T00: Extended node fields --
it("addNode persists extended fields", () => {
const rig = repo.createRig("test-rig");
const node = repo.addNode(rig.id, "worker", {
role: "worker",
runtime: "claude-code",
surfaceHint: "tab:workers",
workspace: "review",
restorePolicy: "checkpoint_only",
packageRefs: ["github:example/pkg@v1", "local:./my-pkg"],
});
expect(node.surfaceHint).toBe("tab:workers");
expect(node.workspace).toBe("review");
expect(node.restorePolicy).toBe("checkpoint_only");
expect(node.packageRefs).toEqual(["github:example/pkg@v1", "local:./my-pkg"]);
});
it("getRig returns nodes with extended fields (parsed packageRefs)", () => {
const rig = repo.createRig("test-rig");
repo.addNode(rig.id, "worker", {
surfaceHint: "tab:main",
packageRefs: ["pkg-a", "pkg-b"],
});
const full = repo.getRig(rig.id);
const node = full!.nodes[0]!;
expect(node.surfaceHint).toBe("tab:main");
expect(node.packageRefs).toEqual(["pkg-a", "pkg-b"]);
expect(Array.isArray(node.packageRefs)).toBe(true);
});
it("addNode with no extended fields: surfaceHint=null, workspace=null, restorePolicy=null, packageRefs=[]", () => {
const rig = repo.createRig("test-rig");
const node = repo.addNode(rig.id, "worker");
expect(node.surfaceHint).toBeNull();
expect(node.workspace).toBeNull();
expect(node.restorePolicy).toBeNull();
expect(node.packageRefs).toEqual([]);
});
});
describe("Wiring regression", () => {
it("createFullTestDb schema includes 007 columns", async () => {
const { createFullTestDb } = await import("./helpers/test-app.js");
const db = createFullTestDb();
const cols = db.prepare("PRAGMA table_info(nodes)").all() as { name: string }[];
const names = cols.map((c) => c.name);
expect(names).toContain("surface_hint");
expect(names).toContain("workspace");
expect(names).toContain("restore_policy");
expect(names).toContain("package_refs");
db.close();
});
});
@@ -0,0 +1,61 @@
import { describe, it, expect, beforeEach, afterEach } from "vitest";
import type Database from "better-sqlite3";
import { createDb } from "../src/db/connection.js";
import { migrate } from "../src/db/migrate.js";
import { coreSchema } from "../src/db/migrations/001_core_schema.js";
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
describe("007_node_spec_fields", () => {
let db: Database.Database;
beforeEach(() => {
db = createDb();
});
afterEach(() => {
db.close();
});
it("migration adds surface_hint column", () => {
migrate(db, [coreSchema, nodeSpecFieldsSchema]);
const cols = db.prepare("PRAGMA table_info(nodes)").all() as { name: string }[];
expect(cols.map((c) => c.name)).toContain("surface_hint");
});
it("migration adds workspace column", () => {
migrate(db, [coreSchema, nodeSpecFieldsSchema]);
const cols = db.prepare("PRAGMA table_info(nodes)").all() as { name: string }[];
expect(cols.map((c) => c.name)).toContain("workspace");
});
it("migration adds restore_policy column", () => {
migrate(db, [coreSchema, nodeSpecFieldsSchema]);
const cols = db.prepare("PRAGMA table_info(nodes)").all() as { name: string }[];
expect(cols.map((c) => c.name)).toContain("restore_policy");
});
it("migration adds package_refs column", () => {
migrate(db, [coreSchema, nodeSpecFieldsSchema]);
const cols = db.prepare("PRAGMA table_info(nodes)").all() as { name: string }[];
expect(cols.map((c) => c.name)).toContain("package_refs");
});
it("existing nodes get NULL for all new columns", () => {
migrate(db, [coreSchema]);
db.prepare("INSERT INTO rigs (id, name) VALUES (?, ?)").run("rig-1", "r01");
db.prepare("INSERT INTO nodes (id, rig_id, logical_id) VALUES (?, ?, ?)").run("node-1", "rig-1", "worker");
migrate(db, [coreSchema, nodeSpecFieldsSchema]);
const node = db.prepare("SELECT surface_hint, workspace, restore_policy, package_refs FROM nodes WHERE id = ?").get("node-1") as {
surface_hint: string | null;
workspace: string | null;
restore_policy: string | null;
package_refs: string | null;
};
expect(node.surface_hint).toBeNull();
expect(node.workspace).toBeNull();
expect(node.restore_policy).toBeNull();
expect(node.package_refs).toBeNull();
});
});
@@ -5,11 +5,12 @@ import { migrate } from "../src/db/migrate.js";
import { coreSchema } from "../src/db/migrations/001_core_schema.js";
import { bindingsSessionsSchema } from "../src/db/migrations/002_bindings_sessions.js";
import { resumeMetadataSchema } from "../src/db/migrations/006_resume_metadata.js";
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
import { SessionRegistry } from "../src/domain/session-registry.js";
function setupDb(): Database.Database {
const db = createDb();
migrate(db, [coreSchema, bindingsSessionsSchema, resumeMetadataSchema]);
migrate(db, [coreSchema, bindingsSessionsSchema, resumeMetadataSchema, nodeSpecFieldsSchema]);
return db;
}
@@ -8,6 +8,7 @@ import { eventsSchema } from "../src/db/migrations/003_events.js";
import { snapshotsSchema } from "../src/db/migrations/004_snapshots.js";
import { checkpointsSchema } from "../src/db/migrations/005_checkpoints.js";
import { resumeMetadataSchema } from "../src/db/migrations/006_resume_metadata.js";
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
import { RigRepository } from "../src/domain/rig-repository.js";
import { SessionRegistry } from "../src/domain/session-registry.js";
import { EventBus } from "../src/domain/event-bus.js";
@@ -18,7 +19,7 @@ import type { PersistedEvent } from "../src/domain/types.js";
function setupDb(): Database.Database {
const db = createDb();
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema]);
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, snapshotsSchema, checkpointsSchema, resumeMetadataSchema, nodeSpecFieldsSchema]);
return db;
}
+2 -1
View File
@@ -8,6 +8,7 @@ import { migrate } from "../src/db/migrate.js";
import { coreSchema } from "../src/db/migrations/001_core_schema.js";
import { bindingsSessionsSchema } from "../src/db/migrations/002_bindings_sessions.js";
import { eventsSchema } from "../src/db/migrations/003_events.js";
import { nodeSpecFieldsSchema } from "../src/db/migrations/007_node_spec_fields.js";
import { RigRepository } from "../src/domain/rig-repository.js";
import { SessionRegistry } from "../src/domain/session-registry.js";
import type { CmuxTransportFactory } from "../src/adapters/cmux.js";
@@ -15,7 +16,7 @@ import type { ExecFn } from "../src/adapters/tmux.js";
function seedDbWithStaleSessions(dbPath: string, rigs: { rigName: string; logicalId: string; sessionName: string }[]) {
const db = createDb(dbPath);
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema]);
migrate(db, [coreSchema, bindingsSessionsSchema, eventsSchema, nodeSpecFieldsSchema]);
const rigRepo = new RigRepository(db);
const sessionRegistry = new SessionRegistry(db);