refactor(types): add GroupId and AgentId branded types to ids.ts

Extend the branded type system with two new domain types for
btmsg/bttask agent and group identifiers. Apply to groups.ts
interfaces including agentToProject() domain crossing cast.
This commit is contained in:
Hibryda 2026-03-11 22:56:52 +01:00
parent 46df7949a7
commit f928abd6ce
3 changed files with 70 additions and 8 deletions

View file

@ -1,8 +1,9 @@
import type { ProviderId } from '../providers/types';
import type { AnchorBudgetScale } from './anchors';
import type { ProjectId, GroupId, AgentId } from './ids';
export interface ProjectConfig {
id: string;
id: ProjectId;
name: string;
identifier: string;
description: string;
@ -35,8 +36,9 @@ export const AGENT_ROLE_ICONS: Record<string, string> = {
/** Convert a GroupAgentConfig to a ProjectConfig for unified rendering */
export function agentToProject(agent: GroupAgentConfig, groupCwd: string): ProjectConfig {
// Agent IDs serve as project IDs in the workspace (agents render as project boxes)
return {
id: agent.id,
id: agent.id as unknown as ProjectId,
name: agent.name,
identifier: agent.role,
description: `${agent.role.charAt(0).toUpperCase() + agent.role.slice(1)} agent`,
@ -58,7 +60,7 @@ export type GroupAgentStatus = 'active' | 'sleeping' | 'stopped';
/** Group-level agent configuration */
export interface GroupAgentConfig {
id: string;
id: AgentId;
name: string;
role: GroupAgentRole;
model?: string;
@ -70,7 +72,7 @@ export interface GroupAgentConfig {
}
export interface GroupConfig {
id: string;
id: GroupId;
name: string;
projects: ProjectConfig[];
/** Group-level orchestration agents (Tier 1) */
@ -80,7 +82,7 @@ export interface GroupConfig {
export interface GroupsFile {
version: number;
groups: GroupConfig[];
activeGroupId: string;
activeGroupId: GroupId;
}
/** Derive a project identifier from a name: lowercase, spaces to dashes */

View file

@ -1,5 +1,11 @@
import { describe, it, expect } from 'vitest';
import { SessionId, ProjectId, type SessionId as SessionIdType, type ProjectId as ProjectIdType } from './ids';
import {
SessionId, ProjectId, GroupId, AgentId,
type SessionId as SessionIdType,
type ProjectId as ProjectIdType,
type GroupId as GroupIdType,
type AgentId as AgentIdType,
} from './ids';
describe('branded types', () => {
describe('SessionId', () => {
@ -41,12 +47,50 @@ describe('branded types', () => {
});
});
describe('GroupId', () => {
it('creates a GroupId from a string', () => {
const id = GroupId('grp-abc');
expect(id).toBe('grp-abc');
});
it('is usable as a Map key', () => {
const map = new Map<GroupIdType, string>();
const id = GroupId('grp-1');
map.set(id, 'test-group');
expect(map.get(id)).toBe('test-group');
});
});
describe('AgentId', () => {
it('creates an AgentId from a string', () => {
const id = AgentId('agent-manager');
expect(id).toBe('agent-manager');
});
it('is usable as a Map key', () => {
const map = new Map<AgentIdType, number>();
const id = AgentId('a1');
map.set(id, 99);
expect(map.get(id)).toBe(99);
});
it('equality works between two AgentIds with same value', () => {
const a = AgentId('a1');
const b = AgentId('a1');
expect(a === b).toBe(true);
});
});
describe('type safety (compile-time)', () => {
it('both types are strings at runtime', () => {
it('all four types are strings at runtime', () => {
const sid = SessionId('s1');
const pid = ProjectId('p1');
const gid = GroupId('g1');
const aid = AgentId('a1');
expect(typeof sid).toBe('string');
expect(typeof pid).toBe('string');
expect(typeof gid).toBe('string');
expect(typeof aid).toBe('string');
});
});
});

View file

@ -1,4 +1,4 @@
// Branded types for domain identifiers — prevents accidental swapping of sessionId/projectId
// Branded types for domain identifiers — prevents accidental swapping of IDs across domains.
// These are compile-time only; at runtime they are plain strings.
/** Unique identifier for an agent session */
@ -7,6 +7,12 @@ export type SessionId = string & { readonly __brand: 'SessionId' };
/** Unique identifier for a project */
export type ProjectId = string & { readonly __brand: 'ProjectId' };
/** Unique identifier for a project group */
export type GroupId = string & { readonly __brand: 'GroupId' };
/** Unique identifier for an agent in the btmsg/bttask system */
export type AgentId = string & { readonly __brand: 'AgentId' };
/** Create a SessionId from a raw string */
export function SessionId(value: string): SessionId {
return value as SessionId;
@ -16,3 +22,13 @@ export function SessionId(value: string): SessionId {
export function ProjectId(value: string): ProjectId {
return value as ProjectId;
}
/** Create a GroupId from a raw string */
export function GroupId(value: string): GroupId {
return value as GroupId;
}
/** Create an AgentId from a raw string */
export function AgentId(value: string): AgentId {
return value as AgentId;
}