Savvi Studio

Hierarchical Token Namespaces Specification

Overview

This specification defines a hierarchical namespace system for dependency injection tokens using ltree-style paths for validation and organization. It builds on the existing container system to provide structured, validated token creation with clear hierarchical relationships.

Goals

  • Structured Namespaces: Organize tokens in a hierarchical tree structure using ltree paths
  • Validation: Ensure token names conform to valid ltree path patterns
  • Type Safety: Maintain full TypeScript support for tokens and namespaces
  • Single Root: Establish a single root namespace for all tokens
  • Domain Organization: Enable domain-specific namespaces with clear boundaries
  • Backward Compatibility: Preserve existing token functionality while adding structure

Core Concepts

Namespace Hierarchy

All tokens exist within a hierarchical namespace structure:

savvi                          # Root namespace
├── core                       # Core infrastructure
│   ├── config                 # Configuration services
│   ├── auth                   # Authentication services
│   ├── graph                  # Graph database services
│   └── session                # Session management
├── app                        # Application layer
│   ├── teams                  # Team domain services
│   │   ├── service            # Team business logic
│   │   └── membership         # Team membership logic
│   ├── tasks                  # Task domain services
│   └── admin                  # Admin functionality
└── test                       # Test-only namespaces
    ├── mocks                  # Mock services
    └── fixtures               # Test fixtures

Token Path Examples

savvi.core.auth.session        # Auth session service
savvi.core.graph.user          # User graph client  
savvi.app.teams.service        # Team service
savvi.app.teams.membership     # Team membership service
savvi.test.mocks.auth          # Auth mock service

Implementation

Enhanced ltree for Namespaces

// src/lib/ltree/index.ts (enhanced with namespace features)
export class ltree {
    // ... existing ltree implementation ...

    /**
     * Check if this path is a valid namespace path
     * (must be at least 2 levels deep: root.domain)
     */
    isValidNamespace(): boolean {
        return this.depth() >= 2;
    }

    /**
     * Check if this path is a valid token path
     * (must be at least 3 levels deep: root.domain.service)
     */
    isValidTokenPath(): boolean {
        return this.depth() >= 3;
    }

    /**
     * Get the root namespace (first segment)
     */
    getRoot(): string {
        return this.path.split('.')[0];
    }

    /**
     * Get the domain namespace (second segment)
     */
    getDomain(): string | null {
        const parts = this.path.split('.');
        return parts.length >= 2 ? parts[1] : null;
    }

    /**
     * Get the service name (last segment)
     */
    getServiceName(): string {
        const parts = this.path.split('.');
        return parts[parts.length - 1];
    }
}

Token Namespace System

// src/lib/container/namespaces.ts
import { ltree } from '@/lib/ltree';

/**
 * Root namespace for all tokens
 */
export const ROOT_NAMESPACE = 'savvi';

/**
 * Token namespace class for creating structured token hierarchies
 */
export class TokenNamespace {
    private constructor(private readonly path: ltree) {}

    /**
     * Create a namespace from an ltree path
     * Validates that the path is a valid namespace
     */
    static from(path: string | ltree): TokenNamespace {
        const ltreePath = typeof path === 'string' ? ltree.parse(path) : path;
        
        if (!ltreePath.isValidNamespace()) {
            throw new Error(`Invalid namespace path: ${ltreePath}. Namespaces must be at least 2 levels deep.`);
        }

        if (ltreePath.getRoot() !== ROOT_NAMESPACE) {
            throw new Error(`All namespaces must start with root namespace '${ROOT_NAMESPACE}', got: ${ltreePath}`);
        }

        return new TokenNamespace(ltreePath);
    }

    /**
     * Create a child namespace
     */
    child(segment: string): TokenNamespace {
        const childPath = this.path.append(segment);
        return new TokenNamespace(childPath);
    }

    /**
     * Create a token within this namespace
     */
    token<T>(name: string): NamespacedToken<T> {
        const tokenPath = this.path.append(name);
        
        if (!tokenPath.isValidTokenPath()) {
            throw new Error(`Invalid token path: ${tokenPath}. Tokens must be at least 3 levels deep.`);
        }

        return new NamespacedToken<T>(tokenPath);
    }

    /**
     * Get the full namespace path
     */
    getPath(): string {
        return this.path.toString();
    }

    /**
     * Get the ltree representation
     */
    getLtree(): ltree {
        return this.path;
    }

    /**
     * Check if this namespace is an ancestor of another
     */
    isAncestorOf(other: TokenNamespace): boolean {
        return this.path.ancestorOrEqual(other.path.toString()) && !this.path.equals(other.path);
    }

    /**
     * Check if this namespace is a descendant of another
     */
    isDescendantOf(other: TokenNamespace): boolean {
        return this.path.descendantOrEqual(other.path.toString()) && !this.path.equals(other.path);
    }

    /**
     * Get all ancestor namespaces
     */
    ancestors(): TokenNamespace[] {
        const ancestorPaths = [...this.path.ancestors()].slice(0, -1); // Exclude self
        return ancestorPaths.map(path => new TokenNamespace(path));
    }

    toString(): string {
        return this.path.toString();
    }
}

/**
 * Namespaced token that includes hierarchy information
 */
export class NamespacedToken<T> {
    private readonly symbol: symbol;

    constructor(private readonly path: ltree) {
        this.symbol = Symbol.for(path.toString()) as any;
    }

    /**
     * Get the underlying symbol (compatible with existing Token<T>)
     */
    getSymbol(): symbol & { __type: T } {
        return this.symbol as symbol & { __type: T };
    }

    /**
     * Get the full token path
     */
    getPath(): string {
        return this.path.toString();
    }

    /**
     * Get the namespace this token belongs to
     */
    getNamespace(): TokenNamespace {
        const namespacePath = this.path.parent();
        if (!namespacePath) {
            throw new Error(`Token ${this.path} has no namespace`);
        }
        return new TokenNamespace(namespacePath);
    }

    /**
     * Get the service name (last segment of path)
     */
    getServiceName(): string {
        return this.path.getServiceName();
    }

    /**
     * Get the domain (second segment of path)
     */
    getDomain(): string | null {
        return this.path.getDomain();
    }

    /**
     * Get the ltree representation
     */
    getLtree(): ltree {
        return this.path;
    }

    toString(): string {
        return this.path.toString();
    }

    /**
     * Enable usage as a regular Symbol in existing code
     */
    valueOf(): symbol {
        return this.symbol;
    }

    /**
     * Symbol.toPrimitive implementation for automatic conversion
     */
    [Symbol.toPrimitive](): symbol {
        return this.symbol;
    }
}

// Type compatibility with existing Token<T>
export type Token<T> = NamespacedToken<T>;

Predefined Namespace Structure

// src/lib/container/namespaces.ts (continued)

/**
 * Root namespace instance
 */
export const rootNamespace = TokenNamespace.from(ROOT_NAMESPACE + '.root');

/**
 * Core infrastructure namespaces
 */
export const coreNamespace = TokenNamespace.from(ROOT_NAMESPACE + '.core');
export const configNamespace = coreNamespace.child('config');
export const authNamespace = coreNamespace.child('auth');
export const graphNamespace = coreNamespace.child('graph');
export const sessionNamespace = coreNamespace.child('session');

/**
 * Application layer namespaces
 */
export const appNamespace = TokenNamespace.from(ROOT_NAMESPACE + '.app');
export const teamsNamespace = appNamespace.child('teams');
export const tasksNamespace = appNamespace.child('tasks');
export const adminNamespace = appNamespace.child('admin');

/**
 * Test namespaces
 */
export const testNamespace = TokenNamespace.from(ROOT_NAMESPACE + '.test');
export const mocksNamespace = testNamespace.child('mocks');
export const fixturesNamespace = testNamespace.child('fixtures');

/**
 * Namespace registry for lookup and validation
 */
export class NamespaceRegistry {
    private static instance: NamespaceRegistry;
    private namespaces = new Map<string, TokenNamespace>();

    private constructor() {
        // Register predefined namespaces
        this.register(rootNamespace);
        this.register(coreNamespace);
        this.register(configNamespace);
        this.register(authNamespace);
        this.register(graphNamespace);
        this.register(sessionNamespace);
        this.register(appNamespace);
        this.register(teamsNamespace);
        this.register(tasksNamespace);
        this.register(adminNamespace);
        this.register(testNamespace);
        this.register(mocksNamespace);
        this.register(fixturesNamespace);
    }

    static getInstance(): NamespaceRegistry {
        if (!this.instance) {
            this.instance = new NamespaceRegistry();
        }
        return this.instance;
    }

    register(namespace: TokenNamespace): void {
        this.namespaces.set(namespace.getPath(), namespace);
    }

    get(path: string): TokenNamespace | undefined {
        return this.namespaces.get(path);
    }

    getOrCreate(path: string): TokenNamespace {
        const existing = this.get(path);
        if (existing) {
            return existing;
        }

        const namespace = TokenNamespace.from(path);
        this.register(namespace);
        return namespace;
    }

    getAllNamespaces(): TokenNamespace[] {
        return Array.from(this.namespaces.values());
    }

    getNamespacesByDomain(domain: string): TokenNamespace[] {
        return Array.from(this.namespaces.values())
            .filter(ns => ns.getLtree().getDomain() === domain);
    }
}

Enhanced Provider System

// src/lib/container/provider.ts (enhanced)
import { ltree } from '@/lib/ltree';
import { NamespacedToken, type Token } from './namespaces';

/**
 * Create a token within a namespace (replaces old token function)
 * 
 * @param namespacePath - Full namespace path (e.g., 'savvi.app.teams.service')
 * @returns Namespaced token
 */
export const token = <T>(namespacePath: string): Token<T> => {
    const path = ltree.parse(namespacePath);
    
    if (!path.isValidTokenPath()) {
        throw new Error(
            `Invalid token path: ${namespacePath}. ` +
            `Tokens must be at least 3 levels deep (root.domain.service).`
        );
    }

    if (path.getRoot() !== 'savvi') {
        throw new Error(
            `All tokens must start with root namespace 'savvi', got: ${namespacePath}`
        );
    }

    return new NamespacedToken<T>(path);
};

/**
 * Create a token from a namespace (convenience method)
 */
export const namespaceToken = <T>(namespace: TokenNamespace, serviceName: string): Token<T> => {
    return namespace.token<T>(serviceName);
};

// Rest of provider.ts remains the same, but now works with NamespacedToken

Domain Token Definitions

Core Tokens

// src/lib/graph/tokens.ts
import { graphNamespace } from '@/lib/container/namespaces';

export const systemGraphToken = graphNamespace.token<GraphClient>('system');
export const userGraphToken = graphNamespace.token<GraphClient>('user');

export const GraphTokens = {
    system: systemGraphToken,
    user: userGraphToken,
} as const;

Teams Tokens

// src/lib/teams/tokens.ts
import { teamsNamespace } from '@/lib/container/namespaces';
import type { TeamService, TeamMembershipService } from './service';

export const teamServiceToken = teamsNamespace.token<TeamService>('service');
export const teamMembershipServiceToken = teamsNamespace.child('membership').token<TeamMembershipService>('service');

export const TeamsTokens = {
    service: teamServiceToken,
    membershipService: teamMembershipServiceToken,
} as const;

// Type exports for cross-module usage
export type TeamServiceToken = typeof teamServiceToken;
export type TeamMembershipServiceToken = typeof teamMembershipServiceToken;

Auth Tokens

// src/lib/auth/tokens.ts  
import { authNamespace } from '@/lib/container/namespaces';
import type { AuthSession, AuthClient, PrincipalService } from './types';

export const authSessionToken = authNamespace.token<AuthSession>('session');
export const authClientToken = authNamespace.token<AuthClient>('client');
export const principalServiceToken = authNamespace.token<PrincipalService>('principal');

export const AuthTokens = {
    session: authSessionToken,
    client: authClientToken,
    principal: principalServiceToken,
} as const;

Usage Examples

Creating New Domain Tokens

// For a new 'analytics' domain
import { appNamespace } from '@/lib/container/namespaces';

// Create analytics namespace
const analyticsNamespace = appNamespace.child('analytics');

// Create tokens within the namespace
export const analyticsServiceToken = analyticsNamespace.token<AnalyticsService>('service');
export const reportsServiceToken = analyticsNamespace.token<ReportsService>('reports');
export const metricsServiceToken = analyticsNamespace.token<MetricsService>('metrics');

// Grouped export
export const AnalyticsTokens = {
    service: analyticsServiceToken,
    reports: reportsServiceToken,
    metrics: metricsServiceToken,
} as const;

Cross-Domain Dependencies

// src/lib/analytics/providers.ts
import { provider, Scope } from '@/lib/container';
import { TeamsTokens } from '@/lib/teams';
import { GraphTokens } from '@/lib/graph';
import { AnalyticsTokens } from './tokens';

export const analyticsServiceProvider = provider(AnalyticsTokens.service)
    .withDependencies(
        GraphTokens.user,
        TeamsTokens.service  // Cross-domain dependency
    )
    .onCreate((deps) => {
        const graphClient = deps[GraphTokens.user];
        const teamsService = deps[TeamsTokens.service];
        return new AnalyticsService(graphClient, teamsService);
    })
    .withScope(Scope.Scoped)
    .build();

Namespace Introspection

// Development tools for namespace exploration
export class NamespaceIntrospector {
    static getTokensByDomain(domain: string): NamespacedToken<any>[] {
        const registry = NamespaceRegistry.getInstance();
        return registry.getNamespacesByDomain(domain)
            .flatMap(namespace => {
                // This would require tracking tokens, which we could add
                return []; // Placeholder
            });
    }

    static getNamespaceHierarchy(): string {
        const registry = NamespaceRegistry.getInstance();
        const namespaces = registry.getAllNamespaces()
            .map(ns => ns.getPath())
            .sort();
        
        return this.formatAsTree(namespaces);
    }

    private static formatAsTree(paths: string[]): string {
        // Implementation to format paths as a tree structure
        // Similar to `tree` command output
        return paths.join('\n'); // Simplified
    }
}

// Usage in development
console.log('Namespace Hierarchy:');
console.log(NamespaceIntrospector.getNamespaceHierarchy());

Testing Integration

Test-Specific Namespaces

// src/lib/container/test-namespaces.ts
import { testNamespace } from './namespaces';

// Create test-specific token namespaces
export const mockAuthNamespace = testNamespace.child('mocks').child('auth');
export const mockGraphNamespace = testNamespace.child('mocks').child('graph');
export const fixtureTeamsNamespace = testNamespace.child('fixtures').child('teams');

// Test tokens
export const mockAuthServiceToken = mockAuthNamespace.token<MockAuthService>('service');
export const mockGraphClientToken = mockGraphNamespace.token<MockGraphClient>('client');
export const teamFixturesToken = fixtureTeamsNamespace.token<TeamFixtures>('data');

Test Utilities

// src/lib/container/test-utils.ts
export class TestTokenFactory {
    /**
     * Create a test token in the test namespace
     */
    static create<T>(domain: string, service: string): Token<T> {
        const testPath = `savvi.test.${domain}.${service}`;
        return token<T>(testPath);
    }

    /**
     * Create a mock token for an existing service
     */
    static createMock<T>(originalToken: Token<T>): Token<T> {
        const originalPath = originalToken.getPath();
        const mockPath = originalPath.replace('savvi.', 'savvi.test.mocks.');
        return token<T>(mockPath);
    }
}

// Usage in tests
const mockTeamService = TestTokenFactory.createMock(TeamsTokens.service);
const testAnalytics = TestTokenFactory.create<TestAnalyticsService>('analytics', 'test_service');

Migration Guide

Phase 1: Foundation

  1. ✅ Move ltree from tests to src/lib/ltree
  2. ⏳ Implement TokenNamespace and NamespacedToken classes
  3. ⏳ Create predefined namespace structure
  4. ⏳ Update token() function to use namespace validation

Phase 2: Core Migration

  1. ⏳ Migrate container tokens to use namespaces
  2. ⏳ Update graph tokens to use graphNamespace
  3. ⏳ Update auth tokens to use authNamespace
  4. ⏳ Verify existing providers still work

Phase 3: Domain Migration

  1. ⏳ Migrate teams tokens to use teamsNamespace
  2. ⏳ Migrate tasks tokens to use tasksNamespace
  3. ⏳ Update cross-domain dependencies
  4. ⏳ Add namespace validation to domain modules

Phase 4: Enhancement

  1. ⏳ Add namespace introspection tools
  2. ⏳ Create test token utilities
  3. ⏳ Add development debugging for namespaces
  4. ⏳ Performance optimization

Backward Compatibility

The system is designed to be backward compatible:

  1. Existing Symbols: All existing Symbol.for() calls continue to work
  2. Token Interface: NamespacedToken<T> implements the same interface as Token<T>
  3. Provider API: No changes needed to existing provider definitions
  4. Container System: Unchanged container behavior

Benefits

  1. Organization: Clear hierarchical structure for all tokens
  2. Validation: Compile-time and runtime validation of token paths
  3. Discoverability: Easy to find related tokens within a domain
  4. Type Safety: Full TypeScript support with namespace awareness
  5. Testing: Dedicated test namespaces for mocks and fixtures
  6. Introspection: Tools to explore and debug token hierarchies
  7. Consistency: Enforced naming conventions across all domains
  8. Scalability: Structured approach that grows with the application

Implementation Timeline

  • Week 1: Implement namespace foundation classes
  • Week 2: Migrate core infrastructure tokens
  • Week 3: Migrate business domain tokens
  • Week 4: Add development tools and testing utilities
  • Week 5: Documentation and team training