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
- ✅ Move ltree from tests to src/lib/ltree
- ⏳ Implement TokenNamespace and NamespacedToken classes
- ⏳ Create predefined namespace structure
- ⏳ Update token() function to use namespace validation
Phase 2: Core Migration
- ⏳ Migrate container tokens to use namespaces
- ⏳ Update graph tokens to use graphNamespace
- ⏳ Update auth tokens to use authNamespace
- ⏳ Verify existing providers still work
Phase 3: Domain Migration
- ⏳ Migrate teams tokens to use teamsNamespace
- ⏳ Migrate tasks tokens to use tasksNamespace
- ⏳ Update cross-domain dependencies
- ⏳ Add namespace validation to domain modules
Phase 4: Enhancement
- ⏳ Add namespace introspection tools
- ⏳ Create test token utilities
- ⏳ Add development debugging for namespaces
- ⏳ Performance optimization
Backward Compatibility
The system is designed to be backward compatible:
- Existing Symbols: All existing
Symbol.for()calls continue to work - Token Interface:
NamespacedToken<T>implements the same interface asToken<T> - Provider API: No changes needed to existing provider definitions
- Container System: Unchanged container behavior
Benefits
- Organization: Clear hierarchical structure for all tokens
- Validation: Compile-time and runtime validation of token paths
- Discoverability: Easy to find related tokens within a domain
- Type Safety: Full TypeScript support with namespace awareness
- Testing: Dedicated test namespaces for mocks and fixtures
- Introspection: Tools to explore and debug token hierarchies
- Consistency: Enforced naming conventions across all domains
- 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