skara-brae-common/scripts/openup/add-analysis-associations.ps1
Jan-Peter von Hunnius c0226df62d
All checks were successful
build / build-test (push) Successful in 16s
openup: EA model built + enriched - baseline, associations, joint design package
Windows/EA session per BUILD-CHECKLIST 0-6 (the planned human-gate path,
executed on the EA machine). Baseline built from EABase.eapx via
build-model.ps1 and verified per checklist 4: 3 actors, 12 UCs with
scenarios, 13 associations, 0 include connectors, 7 SS requirements,
12 realization packages / 36 elements; UC-08 MSS (9 steps) and UC-11
learn spell level (3 steps) match use-case-model.md verbatim.

Script fixes (defects 1-3 of the session log): PS 5.1 hashtable parse
error; realization UC lookup now accepts short and full use-case names.

Enrichment after a manual layout pass - model changes are surgical from
now on, re-running build-model.ps1 is forbidden (checklist 6):
- add-analysis-associations.ps1: 24 analysis associations per mapping
  2a (boundary-controller-entity; UC-04 boundary-entity) rendered via
  DiagramLinks without touching manual layout; 4 uses-dependencies to
  DomainError (UC-05/06/08/12 error paths, model-level)
- add-design-package.ps1: Analysis-Design/Design - joint component
  model (skara-brae-client, sb-api, sb-auth, sb-db, sb-core, sb-shared,
  sb-common; 10 dependencies, rule inward). Joint-model decision
  recorded in mapping 1: one model for server+client+common; client-
  side UC realizations are a future work item.

Markdown sources updated first (drift rule): mapping 1 tree + 2a
interactions + 3 design component model; checklist 6. Exports
regenerated from the model: use-case-model.json new,
uc-realizations.json re-exported (useCase full names, notes merged,
crossCutting added; element content verified equal to the
markdown-to-json generator output).

Open: XMI backup - ExportPackageXMI fails via COM (DISP_E_TYPEMISMATCH,
PS 5.1 + cscript); manual EA UI export per model/README.md option 2.
2026-10-03 19:47:59 +02:00

173 lines
8.2 KiB
PowerShell

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