# Skara Brae — EA analysis-association adder (post-WI-2 enrichment). # SURGICAL: adds connectors + diagram links to EXISTING realization packages. # Never deletes or rebuilds anything - manual layout in the .eapx is preserved. # Re-running build-model.ps1 after a manual layout session is FORBIDDEN (it rebuilds). # # Source of truth: docs/openup/analysis-design/uc-realization-mapping.md §2a # (24 associations + 4 cross-cutting «uses» dependencies to DomainError). $ErrorActionPreference = 'Stop' $repoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path $modelPath = Join-Path $repoRoot 'docs\openup\model\skara-brae.eapx' if (-not (Test-Path $modelPath)) { Write-Output "MISSING $modelPath" exit 1 } # (packageId, fromElement, toElement) per mapping §2a - 24 rows. $associationRows = @( @{ Pkg = 'UC-01 Realization'; From = 'AuthBoundary'; To = 'AccountRegistry' }, @{ Pkg = 'UC-01 Realization'; From = 'AccountRegistry'; To = 'UserAccount' }, @{ Pkg = 'UC-02 Realization'; From = 'TokenService'; To = 'Authenticator' }, @{ Pkg = 'UC-03 Realization'; From = 'CharacterCatalog'; To = 'CharacterFactory' }, @{ Pkg = 'UC-03 Realization'; From = 'CharacterFactory'; To = 'Character' }, @{ Pkg = 'UC-03 Realization'; From = 'CharacterFactory'; To = 'ResourceRegenerator' }, @{ Pkg = 'UC-04 Realization'; From = 'CatalogueBrowser'; To = 'GameData' }, @{ Pkg = 'UC-05 Realization'; From = 'GymBoundary'; To = 'TrainingService' }, @{ Pkg = 'UC-05 Realization'; From = 'TrainingService'; To = 'TrainingGrounds' }, @{ Pkg = 'UC-06 Realization'; From = 'EquipmentBoundary'; To = 'EquipmentService' }, @{ Pkg = 'UC-06 Realization'; From = 'EquipmentService'; To = 'Equipment/Inventory' }, @{ Pkg = 'UC-07 Realization'; From = 'ShopBoundary'; To = 'ShopService' }, @{ Pkg = 'UC-07 Realization'; From = 'ShopService'; To = 'ShopStock' }, @{ Pkg = 'UC-08 Realization'; From = 'AdventureBoundary'; To = 'AdventureService' }, @{ Pkg = 'UC-08 Realization'; From = 'AdventureService'; To = 'CombatEngine' }, @{ Pkg = 'UC-08 Realization'; From = 'AdventureService'; To = 'Combatant' }, @{ Pkg = 'UC-08 Realization'; From = 'AdventureService'; To = 'SpellEffect / BardSong' }, @{ Pkg = 'UC-09 Realization'; From = 'TempleBoundary'; To = 'HealingService' }, @{ Pkg = 'UC-10 Realization'; From = 'SongBoundary'; To = 'SongService' }, @{ Pkg = 'UC-10 Realization'; From = 'SongService'; To = 'BardSong' }, @{ Pkg = 'UC-11 Realization'; From = 'MagicBoundary'; To = 'MagicService' }, @{ Pkg = 'UC-11 Realization'; From = 'MagicService'; To = 'Spell' }, @{ Pkg = 'UC-12 Realization'; From = 'ReviewBoundary'; To = 'ReviewBoard' }, @{ Pkg = 'UC-12 Realization'; From = 'ReviewBoard'; To = 'Progression' } ) # (packageId, controllerName) - 4 «uses» dependencies to DomainError. $domainErrorRows = @( @{ Pkg = 'UC-05 Realization'; From = 'TrainingService' }, @{ Pkg = 'UC-06 Realization'; From = 'EquipmentService' }, @{ Pkg = 'UC-08 Realization'; From = 'AdventureService' }, @{ Pkg = 'UC-12 Realization'; From = 'ReviewBoard' } ) $step = 'start' $app = $null $eaBefore = @(Get-Process -Name EA -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id) function Test-ConnectorExists([object]$clientElement, [int]$supplierId, [string]$type) { $clientElement.Connectors.Refresh() for ($c = 0; $c -lt $clientElement.Connectors.Count; $c++) { $existing = $clientElement.Connectors.GetAt([int]$c) if ($existing.Type -eq $type -and $existing.SupplierID -eq $supplierId) { return $true } } return $false } try { $app = New-Object -ComObject EA.App $app.Visible = $false $repo = $app.Repository $null = $repo.OpenFile($modelPath) $root = $repo.Models.GetAt([int]0) $step = 'resolve Analysis-Design + DomainError' $analysisDesign = $root.Packages.GetByName('Analysis-Design') $architecture = $analysisDesign.Packages.GetByName('Architecture Overview') $architecture.Elements.Refresh() $domainError = $null for ($i = 0; $i -lt $architecture.Elements.Count; $i++) { $candidate = $architecture.Elements.GetAt([int]$i) if ($candidate.Name -eq 'DomainError') { $domainError = $candidate; break } } if (-not $domainError) { throw 'DomainError element not found in Architecture Overview' } $step = 'collect realization package elements' $elementsByPkg = @{} for ($p = 0; $p -lt $analysisDesign.Packages.Count; $p++) { $pkg = $analysisDesign.Packages.GetAt([int]$p) if ($pkg.Name -notlike 'UC-?? Realization') { continue } $pkg.Elements.Refresh() $map = @{} for ($e = 0; $e -lt $pkg.Elements.Count; $e++) { $element = $pkg.Elements.GetAt([int]$e) if ($element.Type -eq 'Class') { $map[$element.Name] = $element } } $elementsByPkg[$pkg.Name] = @{ Map = $map; Package = $pkg } } $step = 'add associations + diagram links' $createdAssoc = 0; $skippedAssoc = 0 foreach ($row in $associationRows) { $entry = $elementsByPkg[$row.Pkg] if (-not $entry) { throw "package not found: $($row.Pkg)" } $client = $entry.Map[$row.From] $supplier = $entry.Map[$row.To] if (-not $client) { throw "element not found in $($row.Pkg): $($row.From)" } if (-not $supplier) { throw "element not found in $($row.Pkg): $($row.To)" } if (Test-ConnectorExists $client $supplier.ElementID 'Association') { $skippedAssoc++; continue } $connector = $client.Connectors.AddNew('', 'Association') $connector.ClientID = $client.ElementID $connector.SupplierID = $supplier.ElementID $null = $connector.Update() $client.Connectors.Refresh() $diagram = $entry.Package.Diagrams.GetAt([int]0) $diagram.DiagramLinks.Refresh() $link = $diagram.DiagramLinks.AddNew('', '') $link.ConnectorID = $connector.ConnectorID $null = $link.Update() $null = $diagram.Update() $createdAssoc++ } $step = 'add DomainError dependencies (model-level, no diagram links - DomainError is not placed on realization diagrams; manual layout stays untouched)' $createdDeps = 0; $skippedDeps = 0 foreach ($row in $domainErrorRows) { $entry = $elementsByPkg[$row.Pkg] $client = $entry.Map[$row.From] if (-not $client) { throw "element not found in $($row.Pkg): $($row.From)" } if (Test-ConnectorExists $client $domainError.ElementID 'Dependency') { $skippedDeps++; continue } $connector = $client.Connectors.AddNew('', 'Dependency') $connector.ClientID = $client.ElementID $connector.SupplierID = $domainError.ElementID $connector.Stereotype = 'uses' $null = $connector.Update() $client.Connectors.Refresh() $createdDeps++ } $step = 'verify' $assocTotal = 0; $depTotal = 0 foreach ($pkgName in $elementsByPkg.Keys) { $entry = $elementsByPkg[$pkgName] foreach ($className in $entry.Map.Keys) { $element = $entry.Map[$className] $element.Connectors.Refresh() for ($c = 0; $c -lt $element.Connectors.Count; $c++) { $connector = $element.Connectors.GetAt([int]$c) if ($connector.Type -eq 'Association') { $assocTotal++ } if ($connector.Type -eq 'Dependency' -and $connector.SupplierID -eq $domainError.ElementID) { $depTotal++ } } } } $assocTotal = $assocTotal / 2 Write-Output "ASSOCIATIONS created=$createdAssoc skipped(existing)=$skippedAssoc -> total=$assocTotal (expect 24)" Write-Output "DOMAINERROR DEPS created=$createdDeps skipped(existing)=$skippedDeps -> total=$depTotal (expect 4)" if ($assocTotal -ne 24 -or $depTotal -ne 4) { Write-Output 'VERIFY FAILED'; exit 1 } $null = $repo.CloseFile() Write-Output 'ALL STEPS OK - associations added surgically; manual layout untouched' } catch { Write-Output "FAILED at [$step]: $($_.Exception.Message)" exit 1 } finally { Get-Process -Name EA -ErrorAction SilentlyContinue | Where-Object { $eaBefore -notcontains $_.Id } | ForEach-Object { Stop-Process -Id $_.Id -Force } }