ModulePlacementService - Architectural Design

Complete design specification for intelligent module placement service

ModulePlacementService - Architectural Design

🎯 Purpose

The ModulePlacementService is the intelligent brain that determines where modules should be placed in the canvas grid. It combines:


📋 Core Responsibilities

  1. Read module placement metadata from ModuleManifest
  2. Suggest optimal placements based on multiple factors
  3. Auto-place with confidence when certainty is high
  4. Validate placements before execution
  5. Learn from user actions (AI training data)

🏗️ Service Architecture

File Location

libs/libs-structural/modular-universe/src/lib/canvas-v2/services/module-placement.service.ts

Dependencies

import { Injectable, inject, computed } from '@angular/core';
import { ModuleRegistryService } from '@libs-core/module-base';
import { LoggerService } from '@libs-core/log-system';
import type { CanvasState, Section } from '../models/canvas-v2.types';
import type { ModulePlacementMetadata, PlacementSuggestion, PlacementValidation } from '../models/module-placement.types';

📝 Type Definitions

1. ModulePlacementMetadata (extends ModuleManifest)

// libs/libs-structural/modular-universe/src/lib/canvas-v2/models/module-placement.types.ts

/**
 * Placement metadata that modules can declare in their manifest
 * This tells the canvas system where the module prefers to be placed
 */
export interface ModulePlacementMetadata {
  /**
   * Preferred location in the canvas
   * Structural modules often have strong preferences
   */
  preferredLocation?: 'header' | 'sidebar' | 'content' | 'footer' | 'floating';
  
  /**
   * Default grid area (for CSS Grid layouts)
   * Example: { row: "1 / 2", column: "1 / -1" }
   */
  defaultGridArea?: {
    row: string;      // "1 / 2" or "span 1"
    column: string;   // "1 / -1" or "span 3"
  };
  
  /**
   * Auto-sizing flag (true for structural modules)
   * When true, module controls its own dimensions
   */
  autoSize?: boolean;
  
  /**
   * Minimum dimensions
   */
  minWidth?: string;      // "300px", "20rem", "30%"
  minHeight?: string;     // "64px", "auto"
  
  /**
   * Maximum dimensions
   */
  maxWidth?: string;
  maxHeight?: string;
  
  /**
   * Responsive overrides for different viewports
   */
  responsive?: {
    mobile?: Partial<ModulePlacementMetadata>;
    tablet?: Partial<ModulePlacementMetadata>;
  };
  
  /**
   * Placement constraints
   */
  constraints?: {
    /** Cannot be nested (must be at root level) */
    requiresRootLevel?: boolean;
    
    /** Must be full width */
    requiresFullWidth?: boolean;
    
    /** Must be fixed/sticky positioned */
    requiresFixedPosition?: boolean;
    
    /** Cannot be placed with these module types */
    incompatibleWith?: string[];  // Module IDs
    
    /** Works best with these module types */
    complementsWell?: string[];   // Module IDs
  };
}

2. PlacementSuggestion

/**
 * A suggested placement for a module
 */
export interface PlacementSuggestion {
  /** Target section ID (or 'new' to create new section) */
  targetSectionId: string | 'new';
  
  /** If 'new', parent section for the new section */
  parentSectionId?: string;
  
  /** Confidence score (0-1) */
  confidence: number;
  
  /** Reason for this suggestion */
  reason: string;
  
  /** Source of suggestion */
  source: 'metadata' | 'pattern' | 'ai-learning' | 'user-history';
  
  /** Section configuration to use */
  sectionConfig?: Partial<Section>;
  
  /** Visual preview data (optional) */
  preview?: {
    location: string;     // "Top of canvas", "Below header"
    layoutType: string;   // "Full width", "Sidebar"
  };
}

3. PlacementValidation

/**
 * Validation result for a proposed placement
 */
export interface PlacementValidation {
  /** Is the placement valid? */
  valid: boolean;
  
  /** Validation errors (if invalid) */
  errors?: string[];
  
  /** Validation warnings (non-blocking) */
  warnings?: string[];
  
  /** Suggestions to fix errors */
  suggestions?: string[];
}

4. Module Manifest Extension

// Extend ModuleManifest from @libs-core/module-base
// This is done via TypeScript declaration merging

declare module '@libs-core/module-base' {
  interface ModuleManifest {
    /**
     * Optional placement metadata
     * Tells the canvas system where this module prefers to be placed
     */
    placement?: ModulePlacementMetadata;
  }
}

🎨 Service Implementation

Core Service Structure

@Injectable({ providedIn: 'root' })
export class ModulePlacementService {
  private readonly moduleRegistry = inject(ModuleRegistryService);
  private readonly logger = inject(LoggerService).child({
    source: 'ModulePlacementService'
  });
  
  // ============================================================================
  // PUBLIC API
  // ============================================================================
  
  /**
   * Get placement metadata for a module
   * @param moduleId - Module ID
   * @returns Placement metadata or null if not defined
   */
  getModulePlacementMetadata(moduleId: string): ModulePlacementMetadata | null {
    const module = this.moduleRegistry.get(moduleId);
    if (!module) {
      this.logger.warn('Module not found', { moduleId });
      return null;
    }
    
    return module.manifest.placement || null;
  }
  
  /**
   * Get intelligent placement suggestions for a module
   * @param moduleId - Module to place
   * @param canvasState - Current canvas state
   * @param userContext - Optional user context (intent, history)
   * @returns Array of placement suggestions, ordered by confidence (highest first)
   */
  getSuggestedPlacements(
    moduleId: string,
    canvasState: CanvasState,
    userContext?: PlacementUserContext
  ): PlacementSuggestion[] {
    const metadata = this.getModulePlacementMetadata(moduleId);
    const suggestions: PlacementSuggestion[] = [];
    
    // 1. Metadata-based suggestions (if module has placement metadata)
    if (metadata) {
      suggestions.push(...this.getMetadataBasedSuggestions(metadata, canvasState));
    }
    
    // 2. Pattern-based suggestions (common layouts)
    suggestions.push(...this.getPatternBasedSuggestions(moduleId, canvasState));
    
    // 3. AI-learned suggestions (from training data)
    // TODO: Implement when AI training system is ready
    // suggestions.push(...this.getAILearnedSuggestions(moduleId, canvasState));
    
    // 4. User history suggestions (if user has placed this module before)
    if (userContext?.userHistory) {
      suggestions.push(...this.getUserHistorySuggestions(moduleId, userContext.userHistory));
    }
    
    // Sort by confidence (highest first)
    suggestions.sort((a, b) => b.confidence - a.confidence);
    
    // Limit to top 5 suggestions
    return suggestions.slice(0, 5);
  }
  
  /**
   * Auto-place module with intelligent defaults
   * @param moduleId - Module to place
   * @param canvasState - Current canvas state
   * @param targetSectionId - Optional specific target section
   * @returns Section configuration for the placed module
   */
  autoPlaceModule(
    moduleId: string,
    canvasState: CanvasState,
    targetSectionId?: string
  ): Section {
    const metadata = this.getModulePlacementMetadata(moduleId);
    const module = this.moduleRegistry.get(moduleId);
    
    if (!module) {
      throw new Error(`Module not found: ${moduleId}`);
    }
    
    // Create section with intelligent defaults
    const section: Partial<Section> = {
      type: 'content',
      content: {
        type: 'module',
        moduleId,
        config: {}
      },
      
      // Sizing based on metadata
      sizing: {
        width: metadata?.autoSize ? 'auto' : '100%',
        height: metadata?.autoSize ? 'auto' : 'auto',
        minWidth: metadata?.minWidth,
        minHeight: metadata?.minHeight,
        maxWidth: metadata?.maxWidth,
        maxHeight: metadata?.maxHeight,
      },
      
      // Positioning based on constraints
      position: {
        type: metadata?.constraints?.requiresFixedPosition ? 'fixed' : 'relative',
      },
      
      // Default layout (flexbox for platform-agnostic)
      layout: {
        engine: 'flex',
        config: {
          direction: 'column',
          justify: 'start',
          align: 'stretch',
        }
      },
      
      // AI metadata for training
      ai: {
        pattern: metadata?.preferredLocation || 'custom',
        generatedBy: 'ai' as const,
        userIntent: 'module-placement',
      }
    };
    
    return section as Section; // Type assertion - full Section will be created by factory
  }
  
  /**
   * Validate a proposed module placement
   * @param moduleId - Module to validate
   * @param sectionId - Target section
   * @param canvasState - Current canvas state
   * @returns Validation result
   */
  validatePlacement(
    moduleId: string,
    sectionId: string,
    canvasState: CanvasState
  ): PlacementValidation {
    const metadata = this.getModulePlacementMetadata(moduleId);
    const section = canvasState.sectionsMap[sectionId];
    const errors: string[] = [];
    const warnings: string[] = [];
    
    if (!section) {
      errors.push(`Section not found: ${sectionId}`);
      return { valid: false, errors };
    }
    
    // Check constraints if metadata exists
    if (metadata?.constraints) {
      const { constraints } = metadata;
      
      // Check root level requirement
      if (constraints.requiresRootLevel && section.parentId) {
        errors.push('This module must be placed at root level');
      }
      
      // Check full width requirement
      if (constraints.requiresFullWidth && section.sizing.width !== '100%') {
        warnings.push('This module works best at full width');
      }
      
      // Check incompatible modules
      if (constraints.incompatibleWith) {
        const conflictingModule = this.findConflictingModule(
          section,
          constraints.incompatibleWith,
          canvasState
        );
        if (conflictingModule) {
          errors.push(`This module cannot be placed with: ${conflictingModule}`);
        }
      }
    }
    
    return {
      valid: errors.length === 0,
      errors: errors.length > 0 ? errors : undefined,
      warnings: warnings.length > 0 ? warnings : undefined,
    };
  }
  
  // ============================================================================
  // PRIVATE HELPERS
  // ============================================================================
  
  /**
   * Get suggestions based on module placement metadata
   */
  private getMetadataBasedSuggestions(
    metadata: ModulePlacementMetadata,
    canvasState: CanvasState
  ): PlacementSuggestion[] {
    const suggestions: PlacementSuggestion[] = [];
    
    // If module has preferred location
    if (metadata.preferredLocation) {
      const targetSection = this.findSectionByLocation(
        metadata.preferredLocation,
        canvasState
      );
      
      if (targetSection) {
        suggestions.push({
          targetSectionId: targetSection.id,
          confidence: 0.9,
          reason: `Module prefers ${metadata.preferredLocation} location`,
          source: 'metadata',
          preview: {
            location: this.getLocationDescription(metadata.preferredLocation),
            layoutType: metadata.autoSize ? 'Auto-sized' : 'Flexible'
          }
        });
      } else {
        // Suggest creating new section
        suggestions.push({
          targetSectionId: 'new',
          parentSectionId: canvasState.rootSection.id,
          confidence: 0.85,
          reason: `Create new ${metadata.preferredLocation} section`,
          source: 'metadata',
        });
      }
    }
    
    return suggestions;
  }
  
  /**
   * Get suggestions based on common patterns
   */
  private getPatternBasedSuggestions(
    moduleId: string,
    canvasState: CanvasState
  ): PlacementSuggestion[] {
    const suggestions: PlacementSuggestion[] = [];
    const module = this.moduleRegistry.get(moduleId);
    
    if (!module) return suggestions;
    
    // Pattern: If canvas is empty, suggest root section
    if (Object.keys(canvasState.sectionsMap).length === 1) {
      suggestions.push({
        targetSectionId: canvasState.rootSection.id,
        confidence: 0.7,
        reason: 'Place in empty canvas',
        source: 'pattern',
      });
    }
    
    // Pattern: Structural modules go at edges
    if (module.manifest.category === 'structural') {
      // Find header/footer sections
      const headerSection = this.findSectionByPattern(canvasState, 'header');
      if (!headerSection) {
        suggestions.push({
          targetSectionId: 'new',
          parentSectionId: canvasState.rootSection.id,
          confidence: 0.75,
          reason: 'Structural modules typically go in header area',
          source: 'pattern',
        });
      }
    }
    
    return suggestions;
  }
  
  /**
   * Get suggestions based on user history
   */
  private getUserHistorySuggestions(
    moduleId: string,
    userHistory: any // TODO: Define UserPlacementHistory type
  ): PlacementSuggestion[] {
    // TODO: Implement when user history tracking is ready
    return [];
  }
  
  /**
   * Find section by location preference
   */
  private findSectionByLocation(
    location: string,
    canvasState: CanvasState
  ): Section | null {
    // Simple implementation - can be enhanced with pattern matching
    const sections = Object.values(canvasState.sectionsMap);
    
    return sections.find(section => 
      section.ai?.pattern === location ||
      section.name?.toLowerCase().includes(location)
    ) || null;
  }
  
  /**
   * Find section by pattern (heuristic)
   */
  private findSectionByPattern(
    canvasState: CanvasState,
    pattern: string
  ): Section | null {
    const sections = Object.values(canvasState.sectionsMap);
    return sections.find(s => s.ai?.pattern === pattern) || null;
  }
  
  /**
   * Check for conflicting modules
   */
  private findConflictingModule(
    section: Section,
    incompatibleModules: string[],
    canvasState: CanvasState
  ): string | null {
    // Check siblings and parent sections for incompatible modules
    const parentId = section.parentId;
    if (!parentId) return null;
    
    const parent = canvasState.sectionsMap[parentId];
    if (!parent) return null;
    
    for (const childId of parent.children) {
      const sibling = canvasState.sectionsMap[childId];
      if (sibling?.content?.moduleId && 
          incompatibleModules.includes(sibling.content.moduleId)) {
        return sibling.content.moduleId;
      }
    }
    
    return null;
  }
  
  /**
   * Get human-readable location description
   */
  private getLocationDescription(location: string): string {
    const descriptions: Record<string, string> = {
      header: 'Top of canvas',
      footer: 'Bottom of canvas',
      sidebar: 'Left or right edge',
      content: 'Main content area',
      floating: 'Overlay/modal'
    };
    
    return descriptions[location] || 'Custom position';
  }
}

🎯 Usage Examples

Example 1: Get Suggestions for Module Placement

// In component
export class CanvasRootComponent {
  private modulePlacement = inject(ModulePlacementService);
  private canvasState = inject(CanvasStateService);
  
  onModuleSelected(moduleId: string): void {
    const canvas = this.canvasState.canvas();
    if (!canvas) return;
    
    // Get intelligent suggestions
    const suggestions = this.modulePlacement.getSuggestedPlacements(
      moduleId,
      canvas
    );
    
    console.log('Placement suggestions:', suggestions);
    
    // If high confidence, auto-place
    if (suggestions[0]?.confidence > 0.9) {
      this.autoPlace(moduleId, suggestions[0]);
    } else {
      // Show suggestions to user
      this.showPlacementOptions(suggestions);
    }
  }
}

Example 2: Auto-Place Module

onModuleDrop(moduleId: string, targetSectionId?: string): void {
  const canvas = this.canvasState.canvas();
  if (!canvas) return;
  
  // Create section with intelligent defaults
  const section = this.modulePlacement.autoPlaceModule(
    moduleId,
    canvas,
    targetSectionId
  );
  
  // Add to canvas
  this.canvasState.addSection(targetSectionId || null, section);
}

Example 3: Validate Before Placement

onPlacementAttempt(moduleId: string, sectionId: string): void {
  const canvas = this.canvasState.canvas();
  if (!canvas) return;
  
  // Validate first
  const validation = this.modulePlacement.validatePlacement(
    moduleId,
    sectionId,
    canvas
  );
  
  if (!validation.valid) {
    this.showErrors(validation.errors!);
    return;
  }
  
  if (validation.warnings) {
    this.showWarnings(validation.warnings);
  }
  
  // Proceed with placement
  this.placeModule(moduleId, sectionId);
}

🤖 AI Training Integration (Future)

Training Data Collection

interface PlacementTrainingData {
  moduleId: string;
  canvasStateBefore: CanvasSnapshot;
  placementChosen: PlacementSuggestion;
  suggestionsShown: PlacementSuggestion[];
  userKeptPlacement: boolean;
  timeTaken: number;
}

Pattern Learning

The service will eventually learn:


✅ Implementation Checklist

Phase 1: Types & Basic Service

Phase 2: Suggestion Engine

Phase 3: Validation

Phase 4: AI Integration (Future)


Status: ✅ Design Complete
Next: Implement types and basic service
Complexity: Medium
Estimated Time: 4-6 hours