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
This commit is contained in:
parent
7a9a5a3e7b
commit
41f1ec2887
6 changed files with 1311 additions and 0 deletions
456
scripts/openup/build-model.ps1
Normal file
456
scripts/openup/build-model.ps1
Normal file
|
|
@ -0,0 +1,456 @@
|
|||
# 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 }
|
||||
}
|
||||
171
scripts/openup/export-model.ps1
Normal file
171
scripts/openup/export-model.ps1
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
# Skara Brae — EA model exporter (WI-2), adapted from keyfob-station's
|
||||
# export-model-json.ps1 (COM walk of the model -> machine-readable JSON).
|
||||
#
|
||||
# Regenerates (generated files — never hand-edit):
|
||||
# docs/openup/model/exports/use-case-model.json (actors, use cases, scenarios, steps)
|
||||
# docs/openup/model/exports/uc-realizations.json (realization packages + elements)
|
||||
# docs/openup/model/exports/xmi-backup.xml (lossless EA XMI 2.1 export of the root)
|
||||
#
|
||||
# Drift rule: markdown (use-case-model.md, uc-realization-mapping.md) and the model must
|
||||
# agree; a mismatch is a defect fixed in the same cycle. uc-realizations.json content must
|
||||
# match what scripts/openup/markdown-to-json.py generated from mapping §2.
|
||||
#
|
||||
# Platform: Windows + installed EA (design §11.c). If COM automation hits friction, the
|
||||
# fallback is EA's manual Publish -> XML export into exports/ (see model/README.md).
|
||||
|
||||
$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 - nothing to export"
|
||||
exit 1
|
||||
}
|
||||
$exportsDir = Join-Path $repoRoot 'docs\openup\model\exports'
|
||||
New-Item -ItemType Directory -Force -Path $exportsDir | Out-Null
|
||||
$ucOutPath = Join-Path $exportsDir 'use-case-model.json'
|
||||
$realOutPath = Join-Path $exportsDir 'uc-realizations.json'
|
||||
$xmiOutPath = Join-Path $exportsDir 'xmi-backup.xml'
|
||||
|
||||
$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)
|
||||
|
||||
# ---------- Requirements / Use Case Model ----------
|
||||
$ucModel = $root.Packages.GetByName('Requirements').Packages.GetByName('Use Case Model')
|
||||
|
||||
$actors = @()
|
||||
$useCases = @()
|
||||
for ($i = 0; $i -lt $ucModel.Elements.Count; $i++) {
|
||||
$element = $ucModel.Elements.GetAt([int]$i)
|
||||
if ($element.Type -eq 'Actor') {
|
||||
$actors += [ordered]@{ name = $element.Name; notes = $element.Notes }
|
||||
}
|
||||
}
|
||||
|
||||
for ($i = 0; $i -lt $ucModel.Elements.Count; $i++) {
|
||||
$element = $ucModel.Elements.GetAt([int]$i)
|
||||
if ($element.Type -ne 'UseCase') { continue }
|
||||
|
||||
$associatedActors = @()
|
||||
for ($c = 0; $c -lt $element.Connectors.Count; $c++) {
|
||||
$connector = $element.Connectors.GetAt([int]$c)
|
||||
if ($connector.Type -ne 'Association') { continue }
|
||||
$otherId = if ($connector.ClientID -eq $element.ElementID) { $connector.SupplierID } else { $connector.ClientID }
|
||||
$other = $repo.GetElementByID($otherId)
|
||||
if ($other.Type -eq 'Actor') { $associatedActors += $other.Name }
|
||||
}
|
||||
|
||||
$scenarios = @()
|
||||
$element.Scenarios.Refresh()
|
||||
for ($s = 0; $s -lt $element.Scenarios.Count; $s++) {
|
||||
$scenario = $element.Scenarios.GetAt([int]$s)
|
||||
$steps = @()
|
||||
$scenario.Steps.Refresh()
|
||||
for ($t = 0; $t -lt $scenario.Steps.Count; $t++) {
|
||||
$scenarioStep = $scenario.Steps.GetAt([int]$t)
|
||||
$steps += [ordered]@{ actor = $scenarioStep.StepType; text = $scenarioStep.Name }
|
||||
}
|
||||
$scenarios += [ordered]@{
|
||||
name = $scenario.Name
|
||||
type = $scenario.Type
|
||||
notes = $scenario.Notes
|
||||
steps = $steps
|
||||
}
|
||||
}
|
||||
|
||||
$useCases += [ordered]@{
|
||||
name = $element.Name
|
||||
notes = $element.Notes
|
||||
actors = $associatedActors
|
||||
scenarios = $scenarios
|
||||
}
|
||||
}
|
||||
|
||||
$ucPayload = [ordered]@{
|
||||
exportedFrom = 'docs/openup/model/skara-brae.eapx (Requirements/Use Case Model)'
|
||||
actors = $actors
|
||||
useCases = $useCases
|
||||
}
|
||||
[System.IO.File]::WriteAllText($ucOutPath, ($ucPayload | ConvertTo-Json -Depth 8), [System.Text.Encoding]::UTF8)
|
||||
Write-Output "exported: $ucOutPath ($($useCases.Count) use cases, $($actors.Count) actors)"
|
||||
|
||||
# ---------- Analysis-Design realizations ----------
|
||||
$analysisDesign = $root.Packages.GetByName('Analysis-Design')
|
||||
$realizations = @()
|
||||
for ($p = 0; $p -lt $analysisDesign.Packages.Count; $p++) {
|
||||
$realizationPackage = $analysisDesign.Packages.GetAt([int]$p)
|
||||
if ($realizationPackage.Name -notlike 'UC-?? Realization') { continue }
|
||||
$realizationPackage.Elements.Refresh()
|
||||
|
||||
$useCaseName = ''
|
||||
$classes = @()
|
||||
for ($e = 0; $e -lt $realizationPackage.Elements.Count; $e++) {
|
||||
$element = $realizationPackage.Elements.GetAt([int]$e)
|
||||
if ($element.Type -eq 'Collaboration') {
|
||||
$element.Connectors.Refresh()
|
||||
for ($c = 0; $c -lt $element.Connectors.Count; $c++) {
|
||||
$connector = $element.Connectors.GetAt([int]$c)
|
||||
if (($connector.Type -eq 'Realization' -or $connector.Type -eq 'Realisation') -and $connector.ClientID -eq $element.ElementID) {
|
||||
$useCaseName = $repo.GetElementByID($connector.SupplierID).Name
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($element.Type -eq 'Class') {
|
||||
$classes += [ordered]@{
|
||||
name = $element.Name
|
||||
stereotype = $element.Stereotype
|
||||
notes = $element.Notes
|
||||
}
|
||||
}
|
||||
}
|
||||
$realizations += [ordered]@{
|
||||
id = $realizationPackage.Name
|
||||
useCase = $useCaseName
|
||||
elements = $classes
|
||||
}
|
||||
}
|
||||
|
||||
$architecture = $analysisDesign.Packages.GetByName('Architecture Overview')
|
||||
$architecture.Elements.Refresh()
|
||||
$crossCutting = @()
|
||||
for ($e = 0; $e -lt $architecture.Elements.Count; $e++) {
|
||||
$element = $architecture.Elements.GetAt([int]$e)
|
||||
$crossCutting += [ordered]@{ name = $element.Name; notes = $element.Notes }
|
||||
}
|
||||
|
||||
$realPayload = [ordered]@{
|
||||
exportedFrom = 'docs/openup/model/skara-brae.eapx (Analysis-Design realization packages)'
|
||||
realizations = $realizations
|
||||
crossCutting = $crossCutting
|
||||
}
|
||||
[System.IO.File]::WriteAllText($realOutPath, ($realPayload | ConvertTo-Json -Depth 8), [System.Text.Encoding]::UTF8)
|
||||
Write-Output "exported: $realOutPath ($($realizations.Count) realizations)"
|
||||
|
||||
# ---------- lossless XMI backup ----------
|
||||
try {
|
||||
$project = $repo.GetProjectInterface()
|
||||
$null = $project.ExportPackageXMI($root.PackageGUID, $xmiOutPath, 1, 0)
|
||||
Write-Output "exported: $xmiOutPath (XMI 2.1 backup of model root '$($root.Name)')"
|
||||
}
|
||||
catch {
|
||||
Write-Output "WARNING: XMI export failed ($($_.Exception.Message)); use EA Publish -> XML manually per model/README.md option 2"
|
||||
}
|
||||
|
||||
$null = $repo.CloseFile()
|
||||
Write-Output 'EXPORT OK - review the exports diff, then commit .eapx + exports/ + scripts/openup/ as one commit'
|
||||
}
|
||||
catch {
|
||||
Write-Output "FAILED: $($_.Exception.ToString())"
|
||||
exit 1
|
||||
}
|
||||
finally {
|
||||
Get-Process -Name EA -ErrorAction SilentlyContinue |
|
||||
Where-Object { $eaBefore -notcontains $_.Id } |
|
||||
ForEach-Object { Stop-Process -Id $_.Id -Force }
|
||||
}
|
||||
149
scripts/openup/markdown-to-json.py
Normal file
149
scripts/openup/markdown-to-json.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate the machine-readable OpenUP model intermediates from the markdown sources.
|
||||
|
||||
Transforms the approved OpenUP artifacts (never hand-edited exports):
|
||||
docs/openup/analysis-design/uc-realization-mapping.md (§2 table)
|
||||
docs/openup/requirements/use-case-model.md (canonical UC names)
|
||||
into:
|
||||
docs/openup/model/exports/uc-realizations.json
|
||||
|
||||
The use-case-model.json intermediate is NOT produced here: the markdown does not
|
||||
carry per-step actor tags, so a faithful transformation requires the EA session
|
||||
(export-model.ps1) or staged decisions. See docs/openup/model/README.md.
|
||||
|
||||
Runs on the standard library only (Linux, no EA/COM required).
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
MAPPING_MD = REPO_ROOT / "docs/openup/analysis-design/uc-realization-mapping.md"
|
||||
UC_MODEL_MD = REPO_ROOT / "docs/openup/requirements/use-case-model.md"
|
||||
OUTPUT_JSON = REPO_ROOT / "docs/openup/model/exports/uc-realizations.json"
|
||||
|
||||
UC_ID_PATTERN = re.compile(r"^(UC-\d{2})\s*(.*)$")
|
||||
|
||||
|
||||
def canonical_use_case_names() -> dict[str, str]:
|
||||
"""UC id -> full use case name, from the use-case-model.md overview table."""
|
||||
names: dict[str, str] = {}
|
||||
for line in UC_MODEL_MD.read_text(encoding="utf-8").splitlines():
|
||||
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
||||
if len(cells) < 2:
|
||||
continue
|
||||
match = UC_ID_PATTERN.match(cells[0])
|
||||
if match:
|
||||
names[match.group(1)] = cells[1]
|
||||
if len(names) != 12:
|
||||
raise SystemExit(
|
||||
f"expected 12 use cases in {UC_MODEL_MD}, found {len(names)}"
|
||||
)
|
||||
return names
|
||||
|
||||
|
||||
def split_table_row(line: str) -> list[str]:
|
||||
return [cell.strip() for cell in line.strip().strip("|").split("|")]
|
||||
|
||||
|
||||
def is_table_row(line: str) -> bool:
|
||||
return line.strip().startswith("|") and line.strip().endswith("|")
|
||||
|
||||
|
||||
def parse_realization_table(mapping_text: str) -> tuple[list[dict], dict[str, str]]:
|
||||
"""Parse the §2 mapping table into per-UC realization entries.
|
||||
|
||||
Returns (realizations, paragraphs) where paragraphs carries the verbatim
|
||||
cross-cutting and dependency-rule paragraphs that follow the table.
|
||||
"""
|
||||
lines = mapping_text.splitlines()
|
||||
section_started = False
|
||||
realizations: list[dict] = []
|
||||
current: dict | None = None
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
if line.startswith("## 2. UC realization mapping"):
|
||||
section_started = True
|
||||
continue
|
||||
if not section_started:
|
||||
continue
|
||||
if line.startswith("## 3."):
|
||||
break
|
||||
if not is_table_row(line):
|
||||
continue
|
||||
cells = split_table_row(line)
|
||||
if cells[0] and set(cells[0]) <= {"-", " ", ":"}:
|
||||
continue
|
||||
if cells[0] in ("UC", "Analysis class"):
|
||||
continue
|
||||
|
||||
uc_cell = cells[0]
|
||||
element = {
|
||||
# Emphasis markers are formatting, not content; element names are
|
||||
# identifiers (e.g. **AdventureService** -> AdventureService).
|
||||
# All other cells stay verbatim.
|
||||
"name": cells[1].replace("**", ""),
|
||||
"stereotype": cells[2],
|
||||
"designElement": cells[3],
|
||||
"notes": cells[4] if len(cells) > 4 else "",
|
||||
}
|
||||
id_match = UC_ID_PATTERN.match(uc_cell)
|
||||
if id_match:
|
||||
current = {"id": f"{id_match.group(1)} Realization", "useCaseId": id_match.group(1), "elements": [element]}
|
||||
realizations.append(current)
|
||||
elif uc_cell == "" and current is not None:
|
||||
current["elements"].append(element)
|
||||
else:
|
||||
raise SystemExit(f"unparseable mapping row {index + 1}: {line!r}")
|
||||
|
||||
paragraphs = {}
|
||||
for paragraph_key, paragraph_prefix in (
|
||||
("crossCutting", "Cross-cutting design elements"),
|
||||
("dependencyRule", "Dependency rule check"),
|
||||
):
|
||||
paragraphs[paragraph_key] = next(
|
||||
(line for line in lines if line.startswith(paragraph_prefix)), ""
|
||||
)
|
||||
return realizations, paragraphs
|
||||
|
||||
|
||||
def build_payload() -> dict:
|
||||
canonical_names = canonical_use_case_names()
|
||||
realizations, paragraphs = parse_realization_table(
|
||||
MAPPING_MD.read_text(encoding="utf-8")
|
||||
)
|
||||
if len(realizations) != 12:
|
||||
raise SystemExit(
|
||||
f"expected 12 realizations in {MAPPING_MD}, found {len(realizations)}"
|
||||
)
|
||||
for realization in realizations:
|
||||
realization["useCase"] = canonical_names[realization.pop("useCaseId")]
|
||||
return {
|
||||
"exportedFrom": (
|
||||
"docs/openup/analysis-design/uc-realization-mapping.md §2, generated by "
|
||||
"scripts/openup/markdown-to-json.py — generated file, never hand-edit"
|
||||
),
|
||||
"realizations": realizations,
|
||||
"crossCutting": paragraphs["crossCutting"],
|
||||
"dependencyRule": paragraphs["dependencyRule"],
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
payload = build_payload()
|
||||
OUTPUT_JSON.parent.mkdir(parents=True, exist_ok=True)
|
||||
OUTPUT_JSON.write_text(
|
||||
json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8"
|
||||
)
|
||||
element_count = sum(len(r["elements"]) for r in payload["realizations"])
|
||||
print(
|
||||
f"wrote {OUTPUT_JSON.relative_to(REPO_ROOT)}: "
|
||||
f"{len(payload['realizations'])} realizations, {element_count} elements"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue