skara-brae-common/scripts/openup/build-model.ps1
Jan-Peter von Hunnius 41f1ec2887 openup: EA model preparation (COD) - checklist, scripts, realizations intermediate
Per design-wi-2-architecture-cleanup.md §11.b/c: keyfob-station pipeline
studied (add-wi19-uc11.ps1, export-model-json.ps1). Platform gap: EA COM
pipeline is Windows-only; this session ran on Linux (no EABase.eapx found,
no pwsh) - .eapx skeleton NOT committed, documented in model/README.md
with the two path options. Landed:
- docs/openup/model/BUILD-CHECKLIST.md: full EA-session checklist
  (packages, 3 actors, 12 UCs with scenarios, 13 associations, 12
  realization packages / 36 elements) derived from use-case-model.md +
  uc-realization-mapping.md
- docs/openup/model/README.md: platform note, status, drift rule
- docs/openup/model/exports/uc-realizations.json: generated from mapping
  §2 via scripts/openup/markdown-to-json.py (tested locally)
- scripts/openup/build-model.ps1, export-model.ps1: adapted from keyfob
  patterns, Windows-gated (human gate per design §11.d)
- scripts/openup/markdown-to-json.py: local markdown -> JSON transform
2026-10-03 17:38:06 +02:00

456 lines
29 KiB
PowerShell

# 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 }
}