Unified Module System - Technical Specification

Technical implementation details for merging Singularity + Modularity into unified module workbench

Unified Module System - Technical Specification

Overview

This specification details how to merge singularity-lib and modularity-lib into a unified Module Workbench that creates real, production-ready modules.


1. Module Context System

1.1 Module Context Service

// libs/libs-structural/unified-module-workbench/src/lib/services/module-context.service.ts

export interface ModuleContext {
  // Identity
  moduleId: string;
  moduleName: string;
  moduleDescription: string;
  moduleCategory: ModuleCategory;
  
  // Data Layer
  entities: EntityDefinition[];
  stateDefinitions: StateDefinition[];
  
  // Logic Layer
  actions: ActionDefinition[];
  activeActionId: string | null;
  
  // UI Layer
  embeddables: EmbeddableDefinition[];
  routes: RouteDefinition[];
  
  // Configuration
  permissions: PermissionDefinition[];
  configuration: ConfigurationSchema;
  
  // Metadata
  createdAt: string;
  updatedAt: string;
  status: 'draft' | 'building' | 'ready' | 'deployed';
}

@Injectable({ providedIn: 'root' })
export class ModuleContextService {
  private readonly storage = inject(GgStorage);
  private readonly logger = inject(LoggerService).child({ source: 'ModuleContext' });
  
  // Reactive state
  private readonly _context = signal<ModuleContext | null>(null);
  readonly context = this._context.asReadonly();
  
  // Computed signals for quick access
  readonly moduleId = computed(() => this._context()?.moduleId ?? null);
  readonly entities = computed(() => this._context()?.entities ?? []);
  readonly actions = computed(() => this._context()?.actions ?? []);
  readonly embeddables = computed(() => this._context()?.embeddables ?? []);
  readonly activeAction = computed(() => {
    const ctx = this._context();
    if (!ctx?.activeActionId) return null;
    return ctx.actions.find(a => a.id === ctx.activeActionId) ?? null;
  });
  
  // Create new module context
  createModule(name: string, category: ModuleCategory): Observable<ModuleContext> {
    const moduleId = `${this.toKebabCase(name)}-${Date.now()}`;
    const context: ModuleContext = {
      moduleId,
      moduleName: name,
      moduleDescription: '',
      moduleCategory: category,
      entities: [],
      stateDefinitions: [],
      actions: [],
      activeActionId: null,
      embeddables: [],
      routes: [],
      permissions: [],
      configuration: {},
      createdAt: new Date().toISOString(),
      updatedAt: new Date().toISOString(),
      status: 'draft'
    };
    
    return from(this.storage.put(`module-context:${moduleId}`, context)).pipe(
      tap(() => this._context.set(context)),
      map(() => context)
    );
  }
  
  // Load existing module context
  loadModule(moduleId: string): Observable<ModuleContext | null> {
    return from(this.storage.get<ModuleContext>(`module-context:${moduleId}`)).pipe(
      tap(ctx => this._context.set(ctx))
    );
  }
  
  // Update context (auto-saves)
  updateContext(updates: Partial<ModuleContext>): Observable<ModuleContext> {
    const current = this._context();
    if (!current) throw new Error('No active module context');
    
    const updated: ModuleContext = {
      ...current,
      ...updates,
      updatedAt: new Date().toISOString()
    };
    
    return from(this.storage.put(`module-context:${current.moduleId}`, updated)).pipe(
      tap(() => this._context.set(updated)),
      map(() => updated)
    );
  }
  
  // Set active action (for Singularity to know what it's editing)
  setActiveAction(actionId: string | null): void {
    const current = this._context();
    if (!current) return;
    
    this._context.set({ ...current, activeActionId: actionId });
  }
}

1.2 Entity Definition Model

// libs/libs-structural/unified-module-workbench/src/lib/models/entity-definition.model.ts

export interface EntityFieldDefinition {
  name: string;
  type: 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'array' | 'object' | 'reference';
  label: string;
  description?: string;
  required: boolean;
  unique?: boolean;
  default?: unknown;
  
  // For enum type
  enumValues?: string[];
  
  // For array type
  arrayItemType?: Omit<EntityFieldDefinition, 'name'>;
  
  // For reference type (relations)
  referenceEntity?: string;
  referenceType?: 'one-to-one' | 'one-to-many' | 'many-to-many';
  
  // Auto-managed fields
  autoGenerate?: 'uuid' | 'timestamp' | 'increment';
  autoUpdate?: 'timestamp';
  
  // Validation
  validation?: {
    min?: number;
    max?: number;
    minLength?: number;
    maxLength?: number;
    pattern?: string;
    custom?: string; // Expression
  };
}

export interface EntityDefinition {
  id: string;
  name: string; // e.g., "User"
  pluralName: string; // e.g., "Users"
  description?: string;
  icon?: string;
  
  fields: EntityFieldDefinition[];
  
  // Generated code preview
  generatedInterface?: string;
  generatedValidator?: string;
}

// Helper to generate TypeScript interface from entity
export function generateEntityInterface(entity: EntityDefinition): string {
  const fieldLines = entity.fields.map(field => {
    const optional = field.required ? '' : '?';
    const type = mapFieldTypeToTS(field);
    return `  ${field.name}${optional}: ${type};`;
  }).join('\n');
  
  return `export interface ${entity.name} {\n${fieldLines}\n}`;
}

function mapFieldTypeToTS(field: EntityFieldDefinition): string {
  switch (field.type) {
    case 'string': return 'string';
    case 'number': return 'number';
    case 'boolean': return 'boolean';
    case 'date': return 'Date';
    case 'enum': return field.enumValues?.map(v => `'${v}'`).join(' | ') ?? 'string';
    case 'array': return `${mapFieldTypeToTS(field.arrayItemType as EntityFieldDefinition)}[]`;
    case 'object': return 'Record<string, unknown>';
    case 'reference': return field.referenceEntity ?? 'string';
    default: return 'unknown';
  }
}

1.3 State Definition Model

// libs/libs-structural/unified-module-workbench/src/lib/models/state-definition.model.ts

export interface StatePropertyDefinition {
  name: string;
  type: string; // TypeScript type
  initialValue: unknown;
  description?: string;
  
  // Source
  source: 'entity' | 'derived' | 'ui' | 'config';
  sourceEntity?: string; // For entity source
  derivedFrom?: string; // Expression for derived
}

export interface StateDefinition {
  id: string;
  name: string; // e.g., "UsersState"
  description?: string;
  
  // Associated entity (optional)
  entityId?: string;
  
  // Properties
  properties: StatePropertyDefinition[];
  
  // Persistence
  persistence: {
    enabled: boolean;
    storageKey: string;
    strategy: 'local' | 'backend' | 'hybrid';
    offlineSupport: boolean;
    syncMode: 'realtime' | 'on-demand' | 'manual';
  };
  
  // Generated service preview
  generatedService?: string;
}

// Helper to generate state service from definition
export function generateStateService(state: StateDefinition, entity?: EntityDefinition): string {
  const entityType = entity?.name ?? 'unknown';
  const storageKey = state.persistence.storageKey;
  
  return `
@Injectable({ providedIn: 'root' })
export class ${state.name}Service {
  private readonly storage = inject(GgStorage);
  
  // State signals
  ${state.properties.map(p => {
    if (p.source === 'entity' && p.type.endsWith('[]')) {
      return `readonly ${p.name} = autoFlashState<${p.type}>('${storageKey}:${p.name}', ${JSON.stringify(p.initialValue)});`;
    }
    return `readonly ${p.name} = signal<${p.type}>(${JSON.stringify(p.initialValue)});`;
  }).join('\n  ')}
  
  // Loading states
  readonly isLoading = signal(false);
  readonly error = signal<string | null>(null);
}
`.trim();
}

2. Integrated Action-Flow System

2.1 Action Definition with Flow Reference

// libs/libs-structural/unified-module-workbench/src/lib/models/action-definition-v2.model.ts

export interface ActionInputDefinition {
  name: string;
  type: ParameterType;
  label: string;
  description?: string;
  required: boolean;
  defaultValue?: unknown;
  validation?: ValidationRules;
}

export interface ActionOutputDefinition {
  type: string; // TypeScript type or entity reference
  description?: string;
  isArray?: boolean;
}

export interface ActionDefinitionV2 {
  id: string;
  name: string;
  displayName: string;
  description?: string;
  category: 'crud' | 'query' | 'mutation' | 'navigation' | 'utility' | 'custom';
  icon?: string;
  
  // Contract
  inputs: ActionInputDefinition[];
  output: ActionOutputDefinition;
  
  // Implementation - ALWAYS a flow
  flowId: string;  // Every action has a Singularity flow
  flowStatus: 'skeleton' | 'in-progress' | 'complete' | 'tested';
  
  // Permissions
  permissions: string[];
  
  // Auto-generated
  isAutoGenerated: boolean; // True for CRUD actions from entity
  sourceEntityId?: string;  // For auto-generated actions
  
  // Testing
  testCases: ActionTestCase[];
  lastTestResult?: TestResult;
}

export interface ActionTestCase {
  id: string;
  name: string;
  input: Record<string, unknown>;
  expectedOutput?: unknown;
  expectedError?: string;
}

2.2 Action-Flow Bridge Service

// libs/libs-structural/unified-module-workbench/src/lib/services/action-flow-bridge.service.ts

@Injectable({ providedIn: 'root' })
export class ActionFlowBridgeService {
  private readonly storage = inject(GgStorage);
  private readonly moduleContext = inject(ModuleContextService);
  private readonly singularityService = inject(SingularityService);
  private readonly logger = inject(LoggerService).child({ source: 'ActionFlowBridge' });
  
  /**
   * Create a new action with its flow skeleton
   */
  createAction(definition: Omit<ActionDefinitionV2, 'flowId' | 'flowStatus'>): Observable<ActionDefinitionV2> {
    const moduleId = this.moduleContext.moduleId();
    if (!moduleId) throw new Error('No active module context');
    
    const actionId = `${moduleId}:${this.toKebabCase(definition.name)}`;
    const flowId = `flow-${actionId}`;
    
    // Create flow skeleton based on action type
    const flowSkeleton = this.createFlowSkeleton(definition, flowId);
    
    return from(this.storage.put(`flow:${flowId}`, flowSkeleton)).pipe(
      switchMap(() => {
        const action: ActionDefinitionV2 = {
          ...definition,
          id: actionId,
          flowId,
          flowStatus: 'skeleton'
        };
        
        // Add action to module context
        return this.moduleContext.updateContext({
          actions: [...this.moduleContext.actions(), action]
        }).pipe(map(() => action));
      })
    );
  }
  
  /**
   * Create flow skeleton based on action type
   */
  private createFlowSkeleton(action: Omit<ActionDefinitionV2, 'flowId' | 'flowStatus'>, flowId: string): LogicFlow {
    const inputBlock: LogicBlock = {
      id: `${flowId}-input`,
      type: 'trigger',
      name: 'Action Input',
      position: { x: 100, y: 200 },
      config: {
        triggerType: 'action',
        actionId: action.id,
        inputs: action.inputs.map(i => ({
          name: i.name,
          type: i.type,
          required: i.required
        }))
      }
    };
    
    const outputBlock: LogicBlock = {
      id: `${flowId}-output`,
      type: 'output',
      name: 'Action Output',
      position: { x: 700, y: 200 },
      config: {
        outputType: action.output.type,
        description: action.output.description
      }
    };
    
    // Add skeleton blocks based on category
    const skeletonBlocks = this.getCategorySkeletonBlocks(action.category, flowId);
    
    return {
      id: flowId,
      name: `${action.displayName} Flow`,
      blocks: [inputBlock, ...skeletonBlocks, outputBlock],
      connections: this.createSkeletonConnections(inputBlock, skeletonBlocks, outputBlock),
      metadata: {
        created: new Date(),
        modified: new Date(),
        author: 'Module Builder',
        description: `Implementation flow for ${action.displayName}`,
        category: 'module-action',
        status: 'skeleton',
        moduleId: this.moduleContext.moduleId()!,
        actionId: action.id
      }
    };
  }
  
  /**
   * Get skeleton blocks based on action category
   */
  private getCategorySkeletonBlocks(category: ActionDefinitionV2['category'], flowId: string): LogicBlock[] {
    switch (category) {
      case 'crud':
        return [
          {
            id: `${flowId}-validate`,
            type: 'function',
            name: '✓ Validate Input',
            position: { x: 250, y: 200 },
            config: { placeholder: true, description: 'Add validation logic here' }
          },
          {
            id: `${flowId}-db`,
            type: 'action',
            name: '💾 Database Operation',
            position: { x: 400, y: 200 },
            config: { placeholder: true, description: 'Add database/API call here' }
          },
          {
            id: `${flowId}-transform`,
            type: 'function',
            name: '🔄 Transform Response',
            position: { x: 550, y: 200 },
            config: { placeholder: true, description: 'Transform data before output' }
          }
        ];
      
      case 'query':
        return [
          {
            id: `${flowId}-fetch`,
            type: 'action',
            name: '📥 Fetch Data',
            position: { x: 300, y: 200 },
            config: { placeholder: true, description: 'Add data fetch logic' }
          },
          {
            id: `${flowId}-filter`,
            type: 'function',
            name: '🔍 Filter/Transform',
            position: { x: 500, y: 200 },
            config: { placeholder: true, description: 'Apply filters and transforms' }
          }
        ];
      
      case 'navigation':
        return [
          {
            id: `${flowId}-resolve`,
            type: 'function',
            name: '🔗 Resolve Route',
            position: { x: 300, y: 200 },
            config: { placeholder: true, description: 'Build navigation route' }
          },
          {
            id: `${flowId}-navigate`,
            type: 'action',
            name: '🚀 Navigate',
            position: { x: 500, y: 200 },
            config: { placeholder: true, description: 'Perform navigation' }
          }
        ];
      
      default:
        return [
          {
            id: `${flowId}-logic`,
            type: 'function',
            name: '⚙️ Custom Logic',
            position: { x: 400, y: 200 },
            config: { placeholder: true, description: 'Add your custom logic here' }
          }
        ];
    }
  }
  
  /**
   * Open action flow in Singularity editor (embedded or full-screen)
   */
  editActionFlow(actionId: string, mode: 'embedded' | 'fullscreen' = 'embedded'): Observable<void> {
    const action = this.moduleContext.actions().find(a => a.id === actionId);
    if (!action) throw new Error(`Action ${actionId} not found`);
    
    // Set active action so Singularity knows the context
    this.moduleContext.setActiveAction(actionId);
    
    // Load flow into Singularity
    return from(this.singularityService.loadFlow(action.flowId)).pipe(
      tap(() => {
        if (mode === 'fullscreen') {
          // Navigate to Singularity with module context
          // Singularity will show module-aware UI
        }
      })
    );
  }
  
  /**
   * Auto-generate CRUD actions from entity
   */
  generateCrudActions(entityId: string): Observable<ActionDefinitionV2[]> {
    const entity = this.moduleContext.entities().find(e => e.id === entityId);
    if (!entity) throw new Error(`Entity ${entityId} not found`);
    
    const crudActions: Omit<ActionDefinitionV2, 'flowId' | 'flowStatus'>[] = [
      {
        id: '', // Will be generated
        name: `get${entity.pluralName}`,
        displayName: `Get ${entity.pluralName}`,
        description: `Retrieve list of ${entity.pluralName.toLowerCase()} with optional filtering`,
        category: 'query',
        icon: 'list',
        inputs: [
          { name: 'limit', type: 'number', label: 'Limit', required: false, defaultValue: 20 },
          { name: 'offset', type: 'number', label: 'Offset', required: false, defaultValue: 0 },
          { name: 'filter', type: 'object', label: 'Filter', required: false }
        ],
        output: { type: `${entity.name}[]`, isArray: true },
        permissions: [`${entity.name.toLowerCase()}:read`],
        isAutoGenerated: true,
        sourceEntityId: entityId,
        testCases: []
      },
      {
        id: '',
        name: `get${entity.name}ById`,
        displayName: `Get ${entity.name} by ID`,
        description: `Retrieve a single ${entity.name.toLowerCase()} by ID`,
        category: 'query',
        icon: 'search',
        inputs: [
          { name: 'id', type: 'string', label: `${entity.name} ID`, required: true }
        ],
        output: { type: entity.name },
        permissions: [`${entity.name.toLowerCase()}:read`],
        isAutoGenerated: true,
        sourceEntityId: entityId,
        testCases: []
      },
      {
        id: '',
        name: `create${entity.name}`,
        displayName: `Create ${entity.name}`,
        description: `Create a new ${entity.name.toLowerCase()}`,
        category: 'crud',
        icon: 'plus',
        inputs: entity.fields
          .filter(f => !f.autoGenerate) // Exclude auto-generated fields
          .map(f => ({
            name: f.name,
            type: mapFieldTypeToParameterType(f.type),
            label: f.label,
            required: f.required,
            defaultValue: f.default
          })),
        output: { type: entity.name },
        permissions: [`${entity.name.toLowerCase()}:create`],
        isAutoGenerated: true,
        sourceEntityId: entityId,
        testCases: []
      },
      {
        id: '',
        name: `update${entity.name}`,
        displayName: `Update ${entity.name}`,
        description: `Update an existing ${entity.name.toLowerCase()}`,
        category: 'crud',
        icon: 'edit',
        inputs: [
          { name: 'id', type: 'string', label: `${entity.name} ID`, required: true },
          ...entity.fields
            .filter(f => !f.autoGenerate && !f.autoUpdate)
            .map(f => ({
              name: f.name,
              type: mapFieldTypeToParameterType(f.type),
              label: f.label,
              required: false // All optional for update
            }))
        ],
        output: { type: entity.name },
        permissions: [`${entity.name.toLowerCase()}:update`],
        isAutoGenerated: true,
        sourceEntityId: entityId,
        testCases: []
      },
      {
        id: '',
        name: `delete${entity.name}`,
        displayName: `Delete ${entity.name}`,
        description: `Delete a ${entity.name.toLowerCase()}`,
        category: 'crud',
        icon: 'trash',
        inputs: [
          { name: 'id', type: 'string', label: `${entity.name} ID`, required: true }
        ],
        output: { type: 'boolean', description: 'Success indicator' },
        permissions: [`${entity.name.toLowerCase()}:delete`],
        isAutoGenerated: true,
        sourceEntityId: entityId,
        testCases: []
      }
    ];
    
    // Create all actions
    return forkJoin(crudActions.map(action => this.createAction(action)));
  }
}

3. Embeddable Widget System

3.1 Enhanced Embeddable Definition

// libs/libs-structural/unified-module-workbench/src/lib/models/embeddable-v2.model.ts

export interface EmbeddableInputBinding {
  source: 'manual' | 'route-param' | 'module-state' | 'config' | 'parent-output';
  
  // For manual: the static value
  manualValue?: unknown;
  
  // For route-param: the param name
  routeParam?: string;
  
  // For module-state: path to state property
  statePath?: string; // e.g., "usersState.currentUser.id"
  
  // For config: config key
  configKey?: string;
  
  // For parent-output: connected widget output
  parentOutputRef?: { widgetId: string; outputName: string };
}

export interface EmbeddableOutputBinding {
  actionId: string; // Module action to trigger
  parameterMapping: Record<string, string>; // Maps output data to action params
}

export interface EmbeddableDefinitionV2 {
  id: string;
  name: string;
  displayName: string;
  description?: string;
  icon?: string;
  category: 'widget' | 'form' | 'list' | 'card' | 'dashboard' | 'navigation' | 'custom';
  
  // Inputs with binding configuration
  inputs: Array<{
    definition: ActionInputDefinition;
    binding: EmbeddableInputBinding;
  }>;
  
  // Outputs with action bindings
  outputs: Array<{
    name: string;
    label: string;
    description?: string;
    dataType: string;
    binding: EmbeddableOutputBinding;
  }>;
  
  // Internal data bindings (from module state)
  dataBindings: Array<{
    propertyName: string;
    statePath: string;
    description?: string;
  }>;
  
  // Template configuration
  template: {
    source: 'visual-builder' | 'page-reference' | 'custom-html';
    
    // For visual-builder
    visualConfig?: VisualTemplateConfig;
    
    // For page-reference
    pageId?: string;
    
    // For custom-html
    customTemplate?: string;
  };
  
  // Permissions required to use this embeddable
  permissions: string[];
  
  // Preview configuration
  preview: {
    mockData: Record<string, unknown>;
    defaultSize: { width: number; height: number };
  };
}

// Visual template configuration (for simple embeddables)
export interface VisualTemplateConfig {
  layout: 'vertical' | 'horizontal' | 'grid' | 'card';
  elements: VisualElement[];
  styling: {
    padding: string;
    gap: string;
    backgroundColor?: string;
    borderRadius?: string;
  };
}

export interface VisualElement {
  type: 'text' | 'image' | 'button' | 'input' | 'list' | 'condition' | 'slot';
  id: string;
  position: number;
  
  // Type-specific config
  config: Record<string, unknown>;
  
  // Binding: where does this element get its data?
  dataBinding?: string; // Path to data property
  
  // Event: what action does this trigger?
  onClick?: string; // Output name
  onChange?: string; // Output name
}

3.2 Embeddable Builder Service

// libs/libs-structural/unified-module-workbench/src/lib/services/embeddable-builder.service.ts

@Injectable({ providedIn: 'root' })
export class EmbeddableBuilderService {
  private readonly moduleContext = inject(ModuleContextService);
  private readonly logger = inject(LoggerService).child({ source: 'EmbeddableBuilder' });
  
  /**
   * Create embeddable from entity (auto-generates list and card widgets)
   */
  generateEntityEmbeddables(entityId: string): Observable<EmbeddableDefinitionV2[]> {
    const entity = this.moduleContext.entities().find(e => e.id === entityId);
    if (!entity) throw new Error(`Entity ${entityId} not found`);
    
    const actions = this.moduleContext.actions();
    
    const embeddables: EmbeddableDefinitionV2[] = [
      // List widget
      {
        id: `${entity.name.toLowerCase()}-list-widget`,
        name: `${entity.name}ListWidget`,
        displayName: `${entity.name} List`,
        description: `Displays a list of ${entity.pluralName.toLowerCase()}`,
        icon: 'list',
        category: 'list',
        inputs: [
          {
            definition: { name: 'limit', type: 'number', label: 'Items per page', required: false, defaultValue: 10 },
            binding: { source: 'manual', manualValue: 10 }
          },
          {
            definition: { name: 'showSearch', type: 'boolean', label: 'Show Search', required: false, defaultValue: true },
            binding: { source: 'manual', manualValue: true }
          }
        ],
        outputs: [
          {
            name: 'onItemClick',
            label: 'Item Click',
            description: `Triggered when a ${entity.name.toLowerCase()} is clicked`,
            dataType: 'string', // Entity ID
            binding: {
              actionId: actions.find(a => a.name === `get${entity.name}ById`)?.id ?? '',
              parameterMapping: { 'id': '$event' }
            }
          },
          {
            name: 'onDelete',
            label: 'Delete',
            description: `Triggered when delete is clicked`,
            dataType: 'string',
            binding: {
              actionId: actions.find(a => a.name === `delete${entity.name}`)?.id ?? '',
              parameterMapping: { 'id': '$event' }
            }
          }
        ],
        dataBindings: [
          {
            propertyName: 'items',
            statePath: `${entity.pluralName.toLowerCase()}State.items`,
            description: `List of ${entity.pluralName.toLowerCase()}`
          },
          {
            propertyName: 'isLoading',
            statePath: `${entity.pluralName.toLowerCase()}State.isLoading`,
            description: 'Loading state'
          }
        ],
        template: {
          source: 'visual-builder',
          visualConfig: this.generateListTemplate(entity)
        },
        permissions: [`${entity.name.toLowerCase()}:read`],
        preview: {
          mockData: this.generateMockData(entity, 5),
          defaultSize: { width: 400, height: 300 }
        }
      },
      
      // Card widget
      {
        id: `${entity.name.toLowerCase()}-card-widget`,
        name: `${entity.name}CardWidget`,
        displayName: `${entity.name} Card`,
        description: `Displays a single ${entity.name.toLowerCase()} as a card`,
        icon: 'card',
        category: 'card',
        inputs: [
          {
            definition: { name: 'entityId', type: 'string', label: `${entity.name} ID`, required: true },
            binding: { source: 'route-param', routeParam: 'id' }
          },
          {
            definition: { name: 'showActions', type: 'boolean', label: 'Show Actions', required: false, defaultValue: true },
            binding: { source: 'manual', manualValue: true }
          }
        ],
        outputs: [
          {
            name: 'onEdit',
            label: 'Edit',
            description: `Navigate to edit ${entity.name.toLowerCase()}`,
            dataType: 'string',
            binding: {
              actionId: '', // Navigate action
              parameterMapping: { 'id': '$entity.id' }
            }
          },
          {
            name: 'onDelete',
            label: 'Delete',
            description: `Delete this ${entity.name.toLowerCase()}`,
            dataType: 'string',
            binding: {
              actionId: actions.find(a => a.name === `delete${entity.name}`)?.id ?? '',
              parameterMapping: { 'id': '$entity.id' }
            }
          }
        ],
        dataBindings: [
          {
            propertyName: 'entity',
            statePath: `${entity.pluralName.toLowerCase()}State.current`,
            description: `Current ${entity.name.toLowerCase()}`
          }
        ],
        template: {
          source: 'visual-builder',
          visualConfig: this.generateCardTemplate(entity)
        },
        permissions: [`${entity.name.toLowerCase()}:read`],
        preview: {
          mockData: this.generateMockData(entity, 1)[0],
          defaultSize: { width: 350, height: 200 }
        }
      }
    ];
    
    return of(embeddables);
  }
  
  private generateListTemplate(entity: EntityDefinition): VisualTemplateConfig {
    // Auto-generate visual template based on entity fields
    const displayFields = entity.fields.slice(0, 4); // First 4 fields
    
    return {
      layout: 'vertical',
      elements: [
        {
          type: 'input',
          id: 'search',
          position: 0,
          config: { placeholder: 'Search...', type: 'search' },
          onChange: 'onSearch'
        },
        {
          type: 'list',
          id: 'items-list',
          position: 1,
          config: {
            itemTemplate: displayFields.map(f => ({
              field: f.name,
              label: f.label
            }))
          },
          dataBinding: 'items',
          onClick: 'onItemClick'
        }
      ],
      styling: {
        padding: '16px',
        gap: '12px'
      }
    };
  }
  
  private generateCardTemplate(entity: EntityDefinition): VisualTemplateConfig {
    return {
      layout: 'card',
      elements: entity.fields.map((f, i) => ({
        type: 'text',
        id: f.name,
        position: i,
        config: { label: f.label },
        dataBinding: `entity.${f.name}`
      })),
      styling: {
        padding: '20px',
        gap: '8px',
        borderRadius: '12px'
      }
    };
  }
  
  private generateMockData(entity: EntityDefinition, count: number): Record<string, unknown>[] {
    // Generate realistic mock data based on field types
    return Array.from({ length: count }, (_, i) => {
      const mock: Record<string, unknown> = {};
      
      entity.fields.forEach(f => {
        switch (f.type) {
          case 'string':
            mock[f.name] = f.name === 'name' ? `Sample ${entity.name} ${i + 1}` :
                          f.name === 'email' ? `user${i + 1}@example.com` :
                          `${f.name}-value-${i + 1}`;
            break;
          case 'number':
            mock[f.name] = i + 1;
            break;
          case 'boolean':
            mock[f.name] = i % 2 === 0;
            break;
          case 'date':
            mock[f.name] = new Date().toISOString();
            break;
          case 'enum':
            mock[f.name] = f.enumValues?.[i % (f.enumValues?.length ?? 1)] ?? 'default';
            break;
          default:
            mock[f.name] = null;
        }
      });
      
      mock['id'] = `mock-${i + 1}`;
      return mock;
    });
  }
}

4. Unified Module Workbench Component

4.1 Main Workbench Component

// libs/libs-structural/unified-module-workbench/src/lib/components/module-workbench/module-workbench.component.ts

type WorkbenchStep = 'models' | 'state' | 'actions' | 'embeddables' | 'routes' | 'test' | 'deploy';

@Component({
  selector: 'lib-module-workbench',
  standalone: true,
  imports: [
    CommonModule,
    FormsModule,
    EntityBuilderComponent,
    StateBuilderComponent,
    ActionBuilderV2Component,
    EmbeddableBuilderV2Component,
    RouteBuilderComponent,
    ModuleTestingComponent,
    EmbeddedSingularityComponent,
    ModulePreviewComponent
  ],
  templateUrl: './module-workbench.component.html',
  styleUrls: ['./module-workbench.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModuleWorkbenchComponent implements OnInit, OnDestroy {
  private readonly moduleContext = inject(ModuleContextService);
  private readonly actionFlowBridge = inject(ActionFlowBridgeService);
  private readonly embeddableBuilder = inject(EmbeddableBuilderService);
  private readonly router = inject(Router);
  private readonly destroyRef = inject(DestroyRef);
  
  // Step management
  readonly steps: WorkbenchStep[] = ['models', 'state', 'actions', 'embeddables', 'routes', 'test', 'deploy'];
  readonly currentStep = signal<WorkbenchStep>('models');
  readonly completedSteps = signal<Set<WorkbenchStep>>(new Set());
  
  // Context signals
  readonly context = this.moduleContext.context;
  readonly moduleId = this.moduleContext.moduleId;
  readonly entities = this.moduleContext.entities;
  readonly actions = this.moduleContext.actions;
  readonly embeddables = this.moduleContext.embeddables;
  
  // UI state
  readonly showSingularity = signal(false);
  readonly singularityMode = signal<'embedded' | 'fullscreen'>('embedded');
  readonly activeActionForFlow = signal<string | null>(null);
  
  // Step completion computed
  readonly stepStatus = computed(() => {
    const ctx = this.context();
    if (!ctx) return {} as Record<WorkbenchStep, 'empty' | 'partial' | 'complete'>;
    
    return {
      models: ctx.entities.length > 0 ? 'complete' : 'empty',
      state: ctx.stateDefinitions.length > 0 ? 'complete' : 'empty',
      actions: this.getActionsStatus(ctx.actions),
      embeddables: ctx.embeddables.length > 0 ? 'complete' : 'empty',
      routes: ctx.routes.length > 0 ? 'complete' : 'empty',
      test: 'empty', // Based on test results
      deploy: 'empty'
    };
  });
  
  ngOnInit(): void {
    // Check for existing module or create new
    const moduleId = this.router.snapshot?.params['moduleId'];
    if (moduleId) {
      this.moduleContext.loadModule(moduleId).subscribe();
    }
  }
  
  // Step navigation
  goToStep(step: WorkbenchStep): void {
    this.currentStep.set(step);
    this.closeSingularity();
  }
  
  nextStep(): void {
    const currentIndex = this.steps.indexOf(this.currentStep());
    if (currentIndex < this.steps.length - 1) {
      this.currentStep.set(this.steps[currentIndex + 1]);
    }
  }
  
  // Entity operations
  async addEntity(entity: EntityDefinition): Promise<void> {
    await firstValueFrom(this.moduleContext.updateContext({
      entities: [...this.entities(), entity]
    }));
    
    // Auto-generate CRUD actions
    await firstValueFrom(this.actionFlowBridge.generateCrudActions(entity.id));
    
    // Auto-generate embeddables
    const embeddables = await firstValueFrom(this.embeddableBuilder.generateEntityEmbeddables(entity.id));
    await firstValueFrom(this.moduleContext.updateContext({
      embeddables: [...this.embeddables(), ...embeddables]
    }));
  }
  
  // Action operations
  async editActionFlow(actionId: string): Promise<void> {
    this.activeActionForFlow.set(actionId);
    this.showSingularity.set(true);
    
    await firstValueFrom(this.actionFlowBridge.editActionFlow(actionId, this.singularityMode()));
  }
  
  closeSingularity(): void {
    this.showSingularity.set(false);
    this.activeActionForFlow.set(null);
    this.moduleContext.setActiveAction(null);
  }
  
  // Test action
  async testAction(actionId: string): Promise<void> {
    // Open action tester modal
  }
  
  private getActionsStatus(actions: ActionDefinitionV2[]): 'empty' | 'partial' | 'complete' {
    if (actions.length === 0) return 'empty';
    const completedCount = actions.filter(a => a.flowStatus === 'complete' || a.flowStatus === 'tested').length;
    if (completedCount === actions.length) return 'complete';
    return 'partial';
  }
}

5. Key Integration Points

5.1 Singularity Context Awareness

// Modify singularity-lib to be context-aware

// In SingularityService:
readonly moduleContext = inject(ModuleContextService, { optional: true });

// When operating in module context:
readonly isModuleMode = computed(() => !!this.moduleContext?.moduleId());

// Get available data sources in module mode:
readonly availableDataSources = computed(() => {
  if (!this.isModuleMode()) return [];
  
  const ctx = this.moduleContext!.context();
  if (!ctx) return [];
  
  return [
    // Module state
    ...ctx.stateDefinitions.flatMap(s => 
      s.properties.map(p => ({
        name: `${s.name}.${p.name}`,
        type: p.type,
        source: 'module-state'
      }))
    ),
    // Action inputs (when editing specific action)
    ...(this.moduleContext!.activeAction()?.inputs ?? []).map(i => ({
      name: `input.${i.name}`,
      type: i.type,
      source: 'action-input'
    })),
    // Other module actions (can call them)
    ...ctx.actions.map(a => ({
      name: `action.${a.name}`,
      type: 'action',
      source: 'module-action'
    }))
  ];
});

5.2 Widget Registry Integration

// When module embeddables are defined, register with widget system

@Injectable({ providedIn: 'root' })
export class ModuleEmbeddableSync {
  private readonly moduleContext = inject(ModuleContextService);
  private readonly widgetRegistry = inject(WidgetRegistryService);
  
  constructor() {
    // Auto-sync embeddables to widget registry
    effect(() => {
      const embeddables = this.moduleContext.embeddables();
      
      for (const emb of embeddables) {
        const widget = this.convertToWidget(emb);
        this.widgetRegistry.registerWidget(widget);
      }
    });
  }
  
  private convertToWidget(emb: EmbeddableDefinitionV2): GenericWidgetDefinition {
    return {
      id: emb.id,
      name: emb.displayName,
      type: 'module-embeddable',
      category: emb.category,
      description: emb.description,
      icon: emb.icon,
      properties: emb.inputs.map(i => ({
        name: i.definition.name,
        type: i.definition.type,
        label: i.definition.label,
        required: i.definition.required,
        defaultValue: i.definition.defaultValue
      })),
      events: emb.outputs.map(o => ({
        name: o.name,
        label: o.label,
        description: o.description
      })),
      source: 'module',
      moduleId: this.moduleContext.moduleId()!,
      // Render using EmbeddableHostComponent
      componentRef: EmbeddableHostComponent
    };
  }
}

6. Migration Path

Phase 1: Foundation (Week 1-2)

  1. Create unified-module-workbench library
  2. Implement ModuleContextService
  3. Implement entity and state builders
  4. Create basic workbench shell

Phase 2: Action-Flow Integration (Week 3-4)

  1. Implement ActionFlowBridgeService
  2. Modify Singularity for context awareness
  3. Implement embedded Singularity view
  4. Auto-generate flow skeletons

Phase 3: Embeddable System (Week 5-6)

  1. Implement enhanced EmbeddableBuilderService
  2. Create visual template builder
  3. Implement input/output binding UI
  4. Sync to widget registry

Phase 4: Testing & Polish (Week 7-8)

  1. Module testing environment
  2. Live preview
  3. Export/import
  4. Documentation

Success Metrics

Metric Target
Can create CRUD module in 10 minutes ✓
Actions have real implementation (flows) ✓
Embeddables have working inputs/outputs ✓
Can replicate user-management module ✓
Zero disconnected flows ✓
Zero empty wrappers ✓

Document: Technical Specification
Status: Ready for Implementation
Session: 12-02-1-2