openup: EA model built + enriched - baseline, associations, joint design package
All checks were successful
build / build-test (push) Successful in 16s

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.
This commit is contained in:
Jan-Peter von Hunnius 2026-10-03 19:47:59 +02:00
parent 201f8f251c
commit c0226df62d
8 changed files with 1296 additions and 300 deletions

View file

@ -0,0 +1,173 @@
# 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 }
}

View file

@ -0,0 +1,180 @@
# Skara Brae — EA Design package builder (post-WI-2 enrichment).
# SURGICAL: creates/rebuilds ONLY Analysis-Design / Design (agent-owned, no manual
# edits). Requirements, realization packages and Architecture Overview are untouched;
# never re-run build-model.ps1 after a manual layout session.
#
# Source of truth: docs/openup/analysis-design/uc-realization-mapping.md §3
# (joint component model: server crates + skara-brae-client + skara-brae-common).
$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
}
$components = @(
@{ Name = 'skara-brae-client';
Notes = 'Presentation tier. Vue 3.5 SPA (TypeScript, Pinia, Vite); monolithic App.vue @ 34dc02f; JWT bearer auth; REST via Vite proxy to :8080. Serves UC-01..UC-12 as the player-facing boundary. Repo: skara-brae-client.' },
@{ Name = 'sb-api';
Notes = 'Boundary/adapter crate (Axum). Routes/handlers, domain_status status mapping, responses (wire-format freeze, NFR-19). May import sb-core, sb-shared, sb-common, sb-db, sb-auth (design note section 3). Carries the boundaries of all UCs per mapping section 2. Known debt: ShopService/HealingService/parts of MagicService still handler-side.' },
@{ Name = 'sb-auth';
Notes = 'Adapter crate. argon2 password hashing + JWT creation/verification. Serves UC-01 (register), UC-02 (authenticate); auth_middleware guards UC-03..UC-12. Imports sb-shared.' },
@{ Name = 'sb-db';
Notes = 'Adapter crate. Persistence via sb-db::Database. Serves persistence of UC-01..UC-03, UC-05..UC-12 state. Imports sb-shared.' },
@{ Name = 'sb-core';
Notes = 'Domain crate (game engine). Controllers/entities: adventure (UC-08), services train/equip/unequip (UC-05/06), character create/level-up/regen (UC-03/12), combat, spells (UC-11), songs (UC-10), data/items catalogue (UC-04). Must not reference axum/http/StatusCode/serde_json json!/tokio/sb-db/sb-api (design note section 3). DomainError error taxonomy (UC-05/06/08/12 error paths).' },
@{ Name = 'sb-shared';
Notes = 'Domain types crate. User, Character, Item, response types. Zero HTTP imports. Entities of all UCs (mapping section 2).' },
@{ Name = 'sb-common';
Notes = 'Bilingual data package (repo skara-brae-common). data/ JSON (items, enemies, spells, crit tables, zones), rust/ crate (include_str + serde), ts/ package, codegen/. UC-04 data source; consumed by sb-core and sb-api; future client type codegen.' }
)
$dependencyRows = @(
@{ From = 'skara-brae-client'; To = 'sb-api' },
@{ From = 'sb-api'; To = 'sb-core' },
@{ From = 'sb-api'; To = 'sb-shared' },
@{ From = 'sb-api'; To = 'sb-db' },
@{ From = 'sb-api'; To = 'sb-auth' },
@{ From = 'sb-api'; To = 'sb-common' },
@{ From = 'sb-core'; To = 'sb-shared' },
@{ From = 'sb-core'; To = 'sb-common' },
@{ From = 'sb-db'; To = 'sb-shared' },
@{ From = 'sb-auth'; To = 'sb-shared' }
)
$positions = @{
'skara-brae-client' = @(40, 340, 40, 140)
'sb-api' = @(440, 740, 40, 140)
'sb-core' = @(840, 1140, 40, 140)
'sb-shared' = @(1240, 1540, 40, 140)
'sb-auth' = @(440, 740, 260, 360)
'sb-db' = @(840, 1140, 260, 360)
'sb-common' = @(1240, 1540, 260, 360)
}
$step = 'start'
$app = $null
$eaBefore = @(Get-Process -Name EA -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Id)
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'
$analysisDesign = $root.Packages.GetByName('Analysis-Design')
$step = 'rebuild Design package (agent-owned only)'
for ($i = 0; $i -lt $analysisDesign.Packages.Count; $i++) {
if (($analysisDesign.Packages.GetAt([int]$i)).Name -eq 'Design') {
$null = $analysisDesign.Packages.DeleteAt([int]$i, $false)
$analysisDesign.Packages.Refresh()
break
}
}
$design = $analysisDesign.Packages.AddNew('Design', 'Package')
$design.Notes = 'Joint component model of the Skara Brae architecture: server crates, skara-brae-client, skara-brae-common. Dependency rule: inward toward sb-core/sb-shared (domain); adapters (sb-api, sb-db, sb-auth) and the client at the edge. Source: uc-realization-mapping.md section 3. Added 2026-10-03; joint-model decision recorded there (client-side UC realization packages = future work item).'
$null = $design.Update()
$analysisDesign.Packages.Refresh()
$step = 'create components'
$elements = @{}
foreach ($component in $components) {
$element = $design.Elements.AddNew($component.Name, 'Component')
$element.Notes = $component.Notes
$null = $element.Update()
$elements[$component.Name] = $element
}
$design.Elements.Refresh()
$step = 'create dependencies'
foreach ($row in $dependencyRows) {
$client = $elements[$row.From]
$supplier = $elements[$row.To]
$connector = $client.Connectors.AddNew('', 'Dependency')
$connector.ClientID = $client.ElementID
$connector.SupplierID = $supplier.ElementID
$null = $connector.Update()
$client.Connectors.Refresh()
}
$step = 'create component diagram'
$diagram = $design.Diagrams.AddNew('Component Architecture', 'Component')
$diagram.Notes = 'Layered joint architecture: client (edge) -> sb-api (boundary) -> domain (sb-core, sb-shared) with sb-common data; adapters sb-auth, sb-db at the edge of the server.'
$null = $diagram.Update()
$design.Diagrams.Refresh()
$diagram = $design.Diagrams.GetByName('Component Architecture')
foreach ($component in $components) {
$coords = $positions[$component.Name]
$diagramObject = $diagram.DiagramObjects.AddNew('', '')
$diagramObject.ElementID = ($elements[$component.Name]).ElementID
$diagramObject.left = $coords[0]; $diagramObject.right = $coords[1]
$diagramObject.top = $coords[2]; $diagramObject.bottom = $coords[3]
$null = $diagramObject.Update()
}
$step = 'create diagram links'
$analysisDesign.Packages.Refresh()
$design = $analysisDesign.Packages.GetByName('Design')
$design.Elements.Refresh()
$elements = @{}
for ($e = 0; $e -lt $design.Elements.Count; $e++) {
$element = $design.Elements.GetAt([int]$e)
if ($element.Type -eq 'Component') { $elements[$element.Name] = $element }
}
$diagram = $design.Diagrams.GetByName('Component Architecture')
foreach ($row in $dependencyRows) {
$client = $elements[$row.From]
$client.Connectors.Refresh()
for ($c = 0; $c -lt $client.Connectors.Count; $c++) {
$connector = $client.Connectors.GetAt([int]$c)
if ($connector.Type -eq 'Dependency') {
$supplier = $elements[$row.To]
if ($connector.SupplierID -eq $supplier.ElementID) {
$diagram.DiagramLinks.Refresh()
$link = $diagram.DiagramLinks.AddNew('', '')
$link.ConnectorID = $connector.ConnectorID
$null = $link.Update()
}
}
}
}
$null = $diagram.Update()
$step = 'verify'
$componentCount = 0; $dependencyCount = 0
$design = $analysisDesign.Packages.GetByName('Design')
$design.Elements.Refresh()
for ($e = 0; $e -lt $design.Elements.Count; $e++) {
$element = $design.Elements.GetAt([int]$e)
if ($element.Type -eq 'Component') { $componentCount++ }
}
foreach ($name in $elements.Keys) {
$element = $elements[$name]
$element.Connectors.Refresh()
for ($c = 0; $c -lt $element.Connectors.Count; $c++) {
$connector = $element.Connectors.GetAt([int]$c)
if ($connector.Type -eq 'Dependency' -and $connector.ClientID -eq $element.ElementID) { $dependencyCount++ }
}
}
Write-Output "VERIFY components=$componentCount (expect 7), dependencies=$dependencyCount (expect 10)"
if ($componentCount -ne 7 -or $dependencyCount -ne 10) { Write-Output 'VERIFY FAILED'; exit 1 }
$null = $repo.CloseFile()
Write-Output 'ALL STEPS OK - Design package created; nothing else touched'
}
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 }
}

View file

@ -305,7 +305,7 @@ try {
@{ 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]) {
@ -375,7 +375,12 @@ try {
$architecture.Elements.Refresh()
$step = 'UC realization packages (from exports/uc-realizations.json)'
$elementsByShortName = @{}
foreach ($useCase in $useCases) { $elementsByShortName[$useCase.Name.Substring(6)] = $elements[$useCase.Name] }
foreach ($realization in $realizations.realizations) {
$supplierElement = $elements[$realization.useCase]
if (-not $supplierElement) { $supplierElement = $elementsByShortName[$realization.useCase] }
if (-not $supplierElement) { throw "use case not found for $($realization.id): $($realization.useCase)" }
$realizationPackage = $analysisDesign.Packages.AddNew($realization.id, 'Package')
$null = $realizationPackage.Update()
$analysisDesign.Packages.Refresh()
@ -385,7 +390,7 @@ try {
$realizationPackage.Elements.Refresh()
$connector = $collaboration.Connectors.AddNew('', 'Realization')
$connector.ClientID = $collaboration.ElementID
$connector.SupplierID = ($elements[$realization.useCase]).ElementID
$connector.SupplierID = $supplierElement.ElementID
$null = $connector.Update()
$collaboration.Connectors.Refresh()