# Skara Brae — EA model builder (WI-2), adapted from keyfob-station's # generate-usecase-model.ps1 / add-wi19-uc11.ps1 patterns (COM automation). # # Design: skara-brae-common docs/openup/analysis-design/design-wi-2-architecture-cleanup.md §11. # Content sources (authoritative): docs/openup/requirements/use-case-model.md, # docs/openup/analysis-design/uc-realization-mapping.md, docs/openup/model/BUILD-CHECKLIST.md. # # Platform: Windows + installed EA. The .eapx skeleton must exist first — copy the EA # installation's EABase.eapx to docs/openup/model/skara-brae.eapx (see model/README.md; # never derive it from keyfob-station's model). # # Scenario content is embedded below as arrays (keyfob fallback pattern, §11.c) because # the markdown does not carry machine-readable per-step actor tags; the human gate # verifies it against use-case-model.md via BUILD-CHECKLIST.md §4. # Realization packages are built from the generated intermediate # docs/openup/model/exports/uc-realizations.json (scripts/openup/markdown-to-json.py). $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 - copy your EA installation's EABase.eapx there first (see docs/openup/model/README.md)" exit 1 } $realizationsPath = Join-Path $repoRoot 'docs\openup\model\exports\uc-realizations.json' $realizations = Get-Content $realizationsPath -Raw -Encoding UTF8 | ConvertFrom-Json $step = 'start' $app = $null $eaBefore = @(Get-Process -Name EA -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id) function Set-ScenarioSteps($scenario, $stepsData) { foreach ($sd in $stepsData) { $scenarioStep = $scenario.Steps.AddNew($sd.Text, $sd.Actor) $null = $scenarioStep.Update() } $scenario.Steps.Refresh() $null = $scenario.Update() $null = $scenario.Update() } try { $app = New-Object -ComObject EA.App $app.Visible = $false $repo = $app.Repository $null = $repo.OpenFile($modelPath) $step = 'rename model root' $root = $repo.Models.GetAt([int]0) if ($root.Name -ne 'Skara Brae') { $root.Name = 'Skara Brae'; $null = $root.Update() } # ---------- Requirements tree ---------- $step = 'remove and recreate Requirements' for ($i = 0; $i -lt $root.Packages.Count; $i++) { if (($root.Packages.GetAt([int]$i)).Name -eq 'Requirements') { $null = $root.Packages.DeleteAt([int]$i, $false) $root.Packages.Refresh() break } } $requirements = $root.Packages.AddNew('Requirements', 'Package') $null = $requirements.Update() $root.Packages.Refresh() $step = 'Vision package' $vision = $requirements.Packages.AddNew('Vision', 'Package') $vision.Notes = 'Product purpose and segments. Human-facing source: docs/openup/requirements/vision.md (markdown is the source of truth; this package carries purpose notes and links, not a full copy).' $null = $vision.Update() $requirements.Packages.Refresh() $step = 'Supplementary Specifications package' $supp = $requirements.Packages.AddNew('Supplementary Specifications', 'Package') $supp.Notes = 'Non-functional requirements. Source: docs/openup/requirements/supplementary-specifications.md; one Requirement element per top-level section, Trace connectors only where the section names a use case.' $null = $supp.Update() $requirements.Packages.Refresh() $suppSections = @( @{ Name = 'SS-1 Performance / Scalability' }, @{ Name = 'SS-2 Reliability / Correctness' }, @{ Name = 'SS-3 Quality Gates (per-commit acceptance)' }, @{ Name = 'SS-4 Data' }, @{ Name = 'SS-5 Security' }, @{ Name = 'SS-6 Constraints' }, @{ Name = 'SS-7 Usability / API' } ) foreach ($section in $suppSections) { $element = $supp.Elements.AddNew($section.Name, 'Requirement') $null = $element.Update() } $supp.Elements.Refresh() # ---------- Use Case Model ---------- $step = 'remove and recreate Use Case Model' for ($i = 0; $i -lt $requirements.Packages.Count; $i++) { if (($requirements.Packages.GetAt([int]$i)).Name -eq 'Use Case Model') { $null = $requirements.Packages.DeleteAt([int]$i, $false) $requirements.Packages.Refresh() break } } $ucModel = $requirements.Packages.AddNew('Use Case Model', 'Package') $ucModel.Notes = 'Actors, use cases and scenarios of the implemented server MVP. Source: docs/openup/requirements/use-case-model.md (normative for status codes and flows).' $null = $ucModel.Update() $requirements.Packages.Refresh() $step = 'create actors' $actors = @( @{ Name = 'Player'; Notes = 'Authenticated user owning one or more characters (JWT role player).' }, @{ Name = 'Administrator'; Notes = 'Player with elevated rights (role = admin); may view any user''s and any character''s resources read-only.' }, @{ Name = 'Guest'; Notes = 'Unauthenticated caller; only register/login are open.' } ) $elements = @{} foreach ($actor in $actors) { $element = $ucModel.Elements.AddNew($actor.Name, 'Actor') $element.Notes = $actor.Notes $null = $element.Update() $elements[$actor.Name] = $element } $ucModel.Elements.Refresh() $step = 'create use cases' $useCases = @( @{ Name = 'UC-01 Register Account'; Notes = 'Open registration. No precondition.' }, @{ Name = 'UC-02 Authenticate'; Notes = 'Login with name and password; issues a JWT with the user''s real role.' }, @{ Name = 'UC-03 Manage Characters'; Notes = 'Create, list and view characters. Admin may act for any user (read-only view).' }, @{ Name = 'UC-04 Browse Game Catalogue'; Notes = 'Races, classes, items (type/quality/slot filters), enemies, adventure zones from static game data.' }, @{ Name = 'UC-05 Train Stat'; Notes = 'Train a stat at a training ground; costs 1 energy + gold. Includes Regenerate Resources.' }, @{ Name = 'UC-06 Manage Equipment'; Notes = 'Equip from inventory / unequip back to inventory.' }, @{ Name = 'UC-07 Trade at Shop'; Notes = 'Buy from (always-stocked + rotating) shop stock or sell inventory items.' }, @{ Name = 'UC-08 Run Adventure'; Notes = 'Zone adventure with weighted enemy pick and turn-based combat. Includes Regenerate Resources, Resolve Combat. Exception: encounter references an unknown enemy -> 500 (post WI-2; previously a panic). WI-2 design driver (sb_core::adventure extraction).' }, @{ Name = 'UC-09 Heal at Temple'; Notes = 'Full or partial healing, cost = amount healed, minimum 1 gold.' }, @{ Name = 'UC-10 Practice Bard Song'; Notes = 'Set or clear the active bard song (Bard only, level-gated).' }, @{ Name = 'UC-11 Study Magic'; Notes = 'View spellbook, learn spell levels (1-7, gold cost), activate/clear the active spell. Caster classes only; spell level = (character level / 3) + 1, uncapped by code.' }, @{ Name = 'UC-12 Level Up at Review Board'; Notes = 'Manual level-up: XP threshold + gold cost (500 per next level), full resource restore on level-up.' } ) foreach ($useCase in $useCases) { $element = $ucModel.Elements.AddNew($useCase.Name, 'UseCase') $element.Notes = $useCase.Notes $null = $element.Update() $elements[$useCase.Name] = $element } $ucModel.Elements.Refresh() $step = 'create associations' function Add-EaConnector([object]$clientName, [object]$supplierName, [string]$type, [string]$stereotype) { $connector = ($elements[$clientName]).Connectors.AddNew('', $type) $connector.ClientID = ($elements[$clientName]).ElementID $connector.SupplierID = ($elements[$supplierName]).ElementID if ($stereotype) { $connector.Stereotype = $stereotype } $null = $connector.Update() ($elements[$clientName]).Connectors.Refresh() } Add-EaConnector 'Guest' 'UC-01 Register Account' 'Association' '' Add-EaConnector 'Guest' 'UC-02 Authenticate' 'Association' '' foreach ($ucName in @('UC-03 Manage Characters', 'UC-04 Browse Game Catalogue', 'UC-05 Train Stat', 'UC-06 Manage Equipment', 'UC-07 Trade at Shop', 'UC-08 Run Adventure', 'UC-09 Heal at Temple', 'UC-10 Practice Bard Song', 'UC-11 Study Magic', 'UC-12 Level Up at Review Board')) { Add-EaConnector 'Player' $ucName 'Association' '' } Add-EaConnector 'Administrator' 'UC-03 Manage Characters' 'Association' '' $step = 'create scenarios' $scenariosByUc = @{ 'UC-01 Register Account' = @( @{ Name = 'Main Success Scenario'; Type = 'Basic Path'; Notes = ''; Steps = @( @{ Text = 'Guest submits name (3-32 chars) and password (>= 6 chars).'; Actor = 'Guest' }, @{ Text = 'System verifies the name is unused -> 409 CONFLICT if taken.'; Actor = 'System' }, @{ Text = 'System hashes the password (argon2), assigns the player role and the next id, persists the user.'; Actor = 'System' }, @{ Text = 'System issues a JWT (HS256, player role) and returns {token, user_id, name, role}.'; Actor = 'System' } ) }, @{ Name = 'Alternate flows'; Type = 'Alternate'; Notes = 'name length invalid -> 400; password too short -> 400; name taken -> 409; hashing/token failure -> 500.'; Steps = @() } ) 'UC-02 Authenticate' = @( @{ Name = 'Main Success Scenario'; Type = 'Basic Path'; Notes = ''; Steps = @( @{ Text = 'Guest submits name and password.'; Actor = 'Guest' }, @{ Text = 'System finds the user (401 if unknown) and verifies the password hash (401 on mismatch).'; Actor = 'System' }, @{ Text = 'System issues a JWT carrying the user''s real role (admin/player) and returns {token, user_id, name, role}.'; Actor = 'System' } ) }, @{ Name = 'Alternate flows'; Type = 'Alternate'; Notes = 'unknown name or wrong password -> 401 UNAUTHORIZED; token creation failure -> 500.'; Steps = @() } ) 'UC-03 Manage Characters' = @( @{ Name = 'Main Success Scenario (create)'; Type = 'Basic Path'; Notes = 'Admin may act for any user.'; Steps = @( @{ Text = 'Player submits character name, race, class for their user id.'; Actor = 'Player' }, @{ Text = 'System validates the target user exists (404) and the trimmed name is 1-32 chars (400).'; Actor = 'System' }, @{ Text = 'System rolls stats (d6+5 base 6-16 per stat +/- race modifiers), computes max HP/energy/nerve from class, grants starting gold (50) and level-1 class spells to casters.'; Actor = 'System' }, @{ Text = 'System assigns the next character id, stores and persists the character; returns it fully.'; Actor = 'System' } ) }, @{ Name = 'list/view'; Type = 'Alternate'; Notes = 'Player lists their characters (Admin: any user''s); the system applies Regenerate Resources to each returned character and returns them. Viewing one character applies regen then returns it.'; Steps = @() }, @{ Name = 'Alternate flows'; Type = 'Alternate'; Notes = 'foreign user/character without admin role -> 403 FORBIDDEN; user/character not found -> 404; invalid name -> 400.'; Steps = @() } ) 'UC-04 Browse Game Catalogue' = @( @{ Name = 'Main Success Scenario'; Type = 'Basic Path'; Notes = ''; Steps = @( @{ Text = 'Player requests the catalogue: races, classes, items (optional type/quality/slot filters), enemies, or adventure zones.'; Actor = 'Player' }, @{ Text = 'System returns static game data loaded from skara-brae-common JSON (class info is enriched with is_caster).'; Actor = 'System' } ) }, @{ Name = 'Alternate flows'; Type = 'Alternate'; Notes = 'single item by unknown id -> 404; filters may return an empty list (not an error).'; Steps = @() } ) 'UC-05 Train Stat' = @( @{ Name = 'Main Success Scenario'; Type = 'Basic Path'; Notes = 'Precondition: character exists and caller may act on it.'; Steps = @( @{ Text = 'Player selects a training ground (strength / dexterity / constitution / intelligence / luck) and submits the stat name.'; Actor = 'Player' }, @{ Text = 'System applies Regenerate Resources.'; Actor = 'System' }, @{ Text = 'System checks energy >= 1 and gold >= training cost (cost grows ~1.3x per 10 stat points).'; Actor = 'System' }, @{ Text = 'System deducts 1 energy and the gold cost, increments the stat; constitution training recomputes max HP (current HP clamped).'; Actor = 'System' }, @{ Text = 'System persists and returns {success, message, character}.'; Actor = 'System' } ) }, @{ Name = 'Alternate flows'; Type = 'Alternate'; Notes = 'foreign character -> 403; character not found -> 404; not enough energy -> 422; not enough gold -> 422; unknown stat name -> 400.'; Steps = @() } ) 'UC-06 Manage Equipment' = @( @{ Name = 'Main Success Scenario (equip)'; Type = 'Basic Path'; Notes = ''; Steps = @( @{ Text = 'Player submits an item id from their inventory.'; Actor = 'Player' }, @{ Text = 'System moves the item from inventory to its slot; a previously equipped item in that slot returns to inventory.'; Actor = 'System' }, @{ Text = 'System persists and returns the updated character.'; Actor = 'System' } ) }, @{ Name = 'unequip'; Type = 'Alternate'; Notes = 'Player submits a slot name; the system moves the equipped item back to inventory.'; Steps = @() }, @{ Name = 'Alternate flows'; Type = 'Alternate'; Notes = 'foreign character -> 403; character/item not found -> 404; item not in inventory -> 400; nothing equipped in slot -> 400.'; Steps = @() } ) 'UC-07 Trade at Shop' = @( @{ Name = 'Main Success Scenario (buy)'; Type = 'Basic Path'; Notes = ''; Steps = @( @{ Text = 'Player views the shop: always-stocked items plus rotating stock (with quantities) alongside their inventory.'; Actor = 'Player' }, @{ Text = 'Player submits buy + item id.'; Actor = 'Player' }, @{ Text = 'System checks gold >= price (422), and for non-always-in-stock items decrements the limited stock (410 GONE if 0).'; Actor = 'System' }, @{ Text = 'System deducts gold, adds the item to inventory, persists character and stock; returns {success, message, gold}.'; Actor = 'System' } ) }, @{ Name = 'sell'; Type = 'Alternate'; Notes = 'Player submits sell + item id; system removes the first matching inventory item, pays half the item value, increments shop stock for non-permanent items.'; Steps = @() }, @{ Name = 'Alternate flows'; Type = 'Alternate'; Notes = 'unknown action or item not owned (sell) -> 400; item not found -> 404; insufficient gold -> 422; stock exhausted -> 410.'; Steps = @() } ) 'UC-08 Run Adventure' = @( @{ Name = 'Main Success Scenario'; Type = 'Basic Path'; Notes = 'Precondition: character exists and caller may act on it.'; Steps = @( @{ Text = 'Player requests an adventure for a zone (default skara_brae_streets).'; Actor = 'Player' }, @{ Text = 'System validates the zone (404) and applies Regenerate Resources.'; Actor = 'System' }, @{ Text = 'System refuses incapacitated characters (HP <= 0 -> 400) and characters with nerve below the zone cost (422); otherwise deducts the nerve cost.'; Actor = 'System' }, @{ Text = 'System draws one enemy from the zone''s weighted encounter table.'; Actor = 'System' }, @{ Text = 'System resolves turn-based combat: open-ended d100 attack rolls, OB vs DB, RoleMaster crit tables with level-gap damage caps, active bard song effects, active spell damage/heal per round.'; Actor = 'System' }, @{ Text = 'On player victory: XP and gold per the enemy definition, character HP set to the last round''s defender HP, and loot dropped per-item chance rolls (only items existing in the catalogue).'; Actor = 'System' }, @{ Text = 'On defeat: character HP set to 0; no rewards.'; Actor = 'System' }, @{ Text = 'Spell-point upkeep is charged whenever a spell was active, win or lose.'; Actor = 'System' }, @{ Text = 'System persists the character and returns {zone, enemy, combat_log, loot} (loot = item display names).'; Actor = 'System' } ) }, @{ Name = 'Alternate flows'; Type = 'Alternate'; Notes = 'foreign character -> 403; character/zone not found -> 404; incapacitated -> 400; insufficient nerve -> 422.'; Steps = @() }, @{ Name = 'Exception'; Type = 'Exception'; Notes = 'encounter references an unknown enemy (data error) -> 500 (post WI-2; previously a panic).'; Steps = @() } ) 'UC-09 Heal at Temple' = @( @{ Name = 'Main Success Scenario'; Type = 'Basic Path'; Notes = ''; Steps = @( @{ Text = 'Player requests healing, optionally partial.'; Actor = 'Player' }, @{ Text = 'System computes missing HP; full heals all, partial heals half (rounded up); cost = amount healed, minimum 1 gold.'; Actor = 'System' }, @{ Text = 'System checks gold (422), deducts it, restores HP, persists; returns {hp, max_hp, gold, healed: true}.'; Actor = 'System' } ) }, @{ Name = 'Alternate flows'; Type = 'Alternate'; Notes = 'foreign character -> 403; not found -> 404; already at full HP -> 400; insufficient gold -> 422.'; Steps = @() } ) 'UC-10 Practice Bard Song' = @( @{ Name = 'Main Success Scenario'; Type = 'Basic Path'; Notes = 'Precondition: character class is Bard.'; Steps = @( @{ Text = 'Player views available songs (level-gated, with combat effect descriptions) and the active song.'; Actor = 'Player' }, @{ Text = 'Player submits a song id; system validates level requirement (422) and sets it active; returns {success, message, active_song}.'; Actor = 'Player' } ) }, @{ Name = 'clear'; Type = 'Alternate'; Notes = 'submitting no song id clears the active song.'; Steps = @() }, @{ Name = 'Alternate flows'; Type = 'Alternate'; Notes = 'non-Bard -> 403; unknown song -> 404; level too low -> 422; foreign character -> 403; not found -> 404.'; Steps = @() } ) 'UC-11 Study Magic' = @( @{ Name = 'Main Success Scenario (view spellbook)'; Type = 'Basic Path'; Notes = 'Precondition: character belongs to a caster class (Mage->magician, Wizard, Conjurer, Sorcerer). Spell level = (character level / 3) + 1 - uncapped by code (level 100 -> spell level 34); learn requests are validated to 1-7.'; Steps = @( @{ Text = 'Player requests their spellbook: caster class, current spell level, spell points, spells up to the achievable level (each marked known), and the learn-cost table for levels 2-7.'; Actor = 'Player' } ) }, @{ Name = 'learn spell level'; Type = 'Alternate'; Notes = ''; Steps = @( @{ Text = 'Player submits a spell level (1-7).'; Actor = 'Player' }, @{ Text = 'System validates caster class (403), level range (400), level not above the character''s current spell level (422), gold >= learn cost (422), and that new spells exist (400).'; Actor = 'System' }, @{ Text = 'System deducts gold, adds all unknown spell ids of that level, recomputes and refills max spell points; returns {learned_count, spell_points, max_spell_points, gold}.'; Actor = 'System' } ) }, @{ Name = 'activate/clear active spell'; Type = 'Alternate'; Notes = ''; Steps = @( @{ Text = 'Player submits a spell id (or empty to clear).'; Actor = 'Player' }, @{ Text = 'System validates the spell is known (403), exists (404), and spell points suffice (422); deducts the spell''s points and sets it active; returns {active_spell, spell_points}.'; Actor = 'System' } ) } ) 'UC-12 Level Up at Review Board' = @( @{ Name = 'Main Success Scenario'; Type = 'Basic Path'; Notes = ''; Steps = @( @{ Text = 'Player requests a level-up.'; Actor = 'Player' }, @{ Text = 'System validates: below max level 100, XP >= 18*100*(level)^2 for the next level (i.e. xp_for_level(next)), gold >= 500*next level.'; Actor = 'System' }, @{ Text = 'System deducts the gold cost, increments the level, recomputes and fully restores HP/energy/nerve and max spell points; persists; returns {success, message, character}.'; Actor = 'System' } ) }, @{ Name = 'Alternate flows'; Type = 'Alternate'; Notes = 'already at max level; not enough XP; not enough gold -> all 422. Foreign character -> 403; not found -> 404.'; Steps = @() } ) ) foreach ($useCase in $useCases) { $ucElement = $elements[$useCase.Name] foreach ($scenarioData in $scenariosByUc[$useCase.Name]) { $scenario = $ucElement.Scenarios.AddNew($scenarioData.Name, $scenarioData.Type) $scenario.Notes = $scenarioData.Notes $null = $scenario.Update() Set-ScenarioSteps $scenario $scenarioData.Steps } $ucElement.Scenarios.Refresh() $null = $ucElement.Update() } $step = 'create use case overview diagram' $diagram = $ucModel.Diagrams.AddNew('Skara Brae Use Case Overview', 'UseCase') $diagram.Notes = 'Actor-to-use-case overview of the implemented server MVP.' $null = $diagram.Update() $ucModel.Diagrams.Refresh() $diagram = $ucModel.Diagrams.GetByName('Skara Brae Use Case Overview') $actorY = 40 foreach ($actor in $actors) { $diagramObject = $diagram.DiagramObjects.AddNew('', '') $diagramObject.ElementID = ($elements[$actor.Name]).ElementID $diagramObject.left = 40; $diagramObject.right = 130 $diagramObject.top = $actorY; $diagramObject.bottom = $actorY + 60 $null = $diagramObject.Update() $actorY += 200 } $ucX = 400; $ucY = 40 foreach ($useCase in $useCases) { $diagramObject = $diagram.DiagramObjects.AddNew('', '') $diagramObject.ElementID = ($elements[$useCase.Name]).ElementID $diagramObject.left = $ucX; $diagramObject.right = $ucX + 300 $diagramObject.top = $ucY; $diagramObject.bottom = $ucY + 60 $null = $diagramObject.Update() $ucY += 100 } $null = $diagram.Update() # ---------- Analysis-Design tree ---------- $step = 'remove and recreate Analysis-Design' for ($i = 0; $i -lt $root.Packages.Count; $i++) { if (($root.Packages.GetAt([int]$i)).Name -eq 'Analysis-Design') { $null = $root.Packages.DeleteAt([int]$i, $false) $root.Packages.Refresh() break } } $analysisDesign = $root.Packages.AddNew('Analysis-Design', 'Package') $null = $analysisDesign.Update() $root.Packages.Refresh() $step = 'Architecture Overview package' $architecture = $analysisDesign.Packages.AddNew('Architecture Overview', 'Package') $architecture.Notes = 'Dependency rule (Clean Architecture): all controllers/entities live in sb-core/sb-shared (domain side, no HTTP imports); all boundaries live in sb-api. The single current violations (ShopService, HealingService, parts of MagicService in sb-api) are recorded as debt with a follow-up question - the model must not paper over them (stale docs are defects). Boundary rules: design note §3.' $null = $architecture.Update() $analysisDesign.Packages.Refresh() $crossCutting = @( @{ Name = 'DomainError'; Notes = 'sb-core::error::DomainError + sb-api::routes::domain_status (status mapping table, design note §4.1) realize the error paths of UC-05/06/08/12.' }, @{ Name = 'Constants'; Notes = 'sb-core::constants realizes NFR "no magic numbers".' }, @{ Name = 'Responses'; Notes = 'sb-api::routes::responses realizes NFR-19 (wire-format freeze).' } ) foreach ($crossElement in $crossCutting) { $element = $architecture.Elements.AddNew($crossElement.Name, 'Class') $element.Notes = $crossElement.Notes $null = $element.Update() } $architecture.Elements.Refresh() $step = 'UC realization packages (from exports/uc-realizations.json)' foreach ($realization in $realizations.realizations) { $realizationPackage = $analysisDesign.Packages.AddNew($realization.id, 'Package') $null = $realizationPackage.Update() $analysisDesign.Packages.Refresh() $collaboration = $realizationPackage.Elements.AddNew($realization.id, 'Collaboration') $null = $collaboration.Update() $realizationPackage.Elements.Refresh() $connector = $collaboration.Connectors.AddNew('', 'Realization') $connector.ClientID = $collaboration.ElementID $connector.SupplierID = ($elements[$realization.useCase]).ElementID $null = $connector.Update() $collaboration.Connectors.Refresh() foreach ($mappingElement in $realization.elements) { $classElement = $realizationPackage.Elements.AddNew($mappingElement.name, 'Class') $classElement.Stereotype = $mappingElement.stereotype $classElement.Notes = "Design element: $($mappingElement.designElement). $($mappingElement.notes)" $null = $classElement.Update() } $realizationPackage.Elements.Refresh() $realizationDiagram = $realizationPackage.Diagrams.AddNew($realization.id, 'Class') $null = $realizationDiagram.Update() $realizationPackage.Diagrams.Refresh() $diagramObj = $realizationPackage.Diagrams.GetByName($realization.id) $classY = 40 foreach ($mappingElement in $realization.elements) { $placed = $false for ($e = 0; $e -lt $realizationPackage.Elements.Count; $e++) { $candidate = $realizationPackage.Elements.GetAt([int]$e) if ($candidate.Name -eq $mappingElement.name -and $candidate.Type -eq 'Class') { $do = $diagramObj.DiagramObjects.AddNew('', '') $do.ElementID = $candidate.ElementID $do.left = 40; $do.right = 300 $do.top = $classY; $do.bottom = $classY + 60 $null = $do.Update() $classY += 100 $placed = $true break } } if (-not $placed) { throw "class not found in $($realization.id): $($mappingElement.name)" } } $null = $diagramObj.Update() } # ---------- verify ---------- $step = 'verify' $null = $repo.CloseFile() $null = $repo.OpenFile($modelPath) $checkUc = $repo.Models.GetAt([int]0).Packages.GetByName('Requirements').Packages.GetByName('Use Case Model') $checkUc.Elements.Refresh() $actorCount = 0; $ucCount = 0 for ($i = 0; $i -lt $checkUc.Elements.Count; $i++) { $element = $checkUc.Elements.GetAt([int]$i) if ($element.Type -eq 'Actor') { $actorCount++ } if ($element.Type -eq 'UseCase') { $ucCount++ } } $checkAd = $repo.Models.GetAt([int]0).Packages.GetByName('Analysis-Design') $checkAd.Packages.Refresh() $realizationCount = 0 for ($i = 0; $i -lt $checkAd.Packages.Count; $i++) { if (($checkAd.Packages.GetAt([int]$i)).Name -like 'UC-?? Realization') { $realizationCount++ } } Write-Output "VERIFY actors=$actorCount (expect 3), useCases=$ucCount (expect 12), realizationPackages=$realizationCount (expect 12)" if ($actorCount -ne 3 -or $ucCount -ne 12 -or $realizationCount -ne 12) { Write-Output 'VERIFY FAILED'; exit 1 } $null = $repo.CloseFile() Write-Output 'ALL STEPS OK - save the model in EA if prompted, then run export-model.ps1 and BUILD-CHECKLIST.md §4' } 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 } }