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

@ -16,10 +16,18 @@ Model: Skara Brae
│ └── Supplementary Specifications (from requirements/supplementary-specifications.md) │ └── Supplementary Specifications (from requirements/supplementary-specifications.md)
└── Analysis-Design └── Analysis-Design
├── Architecture Overview (crate/layer diagram, dependency rule) ├── Architecture Overview (crate/layer diagram, dependency rule)
└── UC-01 Realization … UC-12 Realization (one package per UC: analysis classes, ├── UC-01 Realization … UC-12 Realization (one package per UC: analysis classes,
design elements, realization table below) │ design elements, realization table below)
└── Design (component model of the joint architecture — server
crates + skara-brae-client + skara-brae-common; §3)
``` ```
Joint-model decision (2026-10-03): one EA model covers the whole product — server, client
and common — because the use cases are player-facing and Requirement→realization
traceability spans tiers. Client-side UC realization packages (Vue boundary elements per
UC) are a future work item: no markdown source exists yet; they are NOT invented here.
Today the client enters the model via the §4 component model only.
## 2. UC realization mapping (analysis classes → design elements) ## 2. UC realization mapping (analysis classes → design elements)
Legend — **Analysis class** (OpenUP stereotype): `«controller»`, `«entity»`, `«boundary»`. **Design element**: crate::module::item (Rust). "WI-2" marks elements introduced or re-homed by this work item; everything else exists at 899fde8. Legend — **Analysis class** (OpenUP stereotype): `«controller»`, `«entity»`, `«boundary»`. **Design element**: crate::module::item (Rust). "WI-2" marks elements introduced or re-homed by this work item; everything else exists at 899fde8.
@ -67,7 +75,76 @@ Cross-cutting design elements (WI-2): `sb-core::error::DomainError` + `sb-api::r
Dependency rule check (Clean Architecture): all controllers/entities above 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). Dependency rule check (Clean Architecture): all controllers/entities above 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).
## 3. Research summary — keyfob-station EA pipeline (reference) ### 2a. Analysis interactions (associations + dependencies, added 2026-10-03)
Within each realization package the analysis classes collaborate per the robustness rule
boundary ↔ controller ↔ entity (boundary never touches entity directly — exception: UC-04
has no controller, its boundary reads the static catalogue entity). Each association serves
the UC's Basic Path steps (boundary receives the actor request, controller applies the
rules, entities hold the state). Cross-cutting: the four `«uses»` dependencies realize the
error paths (Alternate/Exception flows) named in the cross-cutting paragraph above.
| UC | From | To | Type | Serves |
|---|---|---|---|---|
| UC-01 | AuthBoundary | AccountRegistry | Association | MSS steps 1–4 |
| UC-01 | AccountRegistry | UserAccount | Association | MSS steps 3–4 |
| UC-02 | TokenService | Authenticator | Association | MSS steps 1–3 |
| UC-03 | CharacterCatalog | CharacterFactory | Association | MSS (create) steps 1–4 |
| UC-03 | CharacterFactory | Character | Association | MSS steps 3–4 |
| UC-03 | CharacterFactory | ResourceRegenerator | Association | list/view (regen on return) |
| UC-04 | CatalogueBrowser | GameData | Association (no controller in this realization) | MSS steps 1–2 |
| UC-05 | GymBoundary | TrainingService | Association | MSS steps 1–5 |
| UC-05 | TrainingService | TrainingGrounds | Association | MSS steps 3–4 |
| UC-06 | EquipmentBoundary | EquipmentService | Association | MSS (equip) + unequip |
| UC-06 | EquipmentService | Equipment/Inventory | Association | MSS steps 2–3 |
| UC-07 | ShopBoundary | ShopService | Association | MSS (buy) + sell |
| UC-07 | ShopService | ShopStock | Association | MSS steps 3–4 |
| UC-08 | AdventureBoundary | AdventureService | Association | MSS steps 1–9 |
| UC-08 | AdventureService | CombatEngine | Association | MSS step 5 |
| UC-08 | AdventureService | Combatant | Association | MSS step 5 |
| UC-08 | AdventureService | SpellEffect / BardSong | Association | MSS steps 5, 8 |
| UC-09 | TempleBoundary | HealingService | Association | MSS steps 1–3 |
| UC-10 | SongBoundary | SongService | Association | MSS steps 1–2 + clear |
| UC-10 | SongService | BardSong | Association | MSS steps 1–2 |
| UC-11 | MagicBoundary | MagicService | Association | view + learn + activate |
| UC-11 | MagicService | Spell | Association | learn + activate steps |
| UC-12 | ReviewBoundary | ReviewBoard | Association | MSS steps 1–3 |
| UC-12 | ReviewBoard | Progression | Association | MSS step 2 |
The authoritative connector count is **24 associations** (UC-01:2, UC-02:1, UC-03:3,
UC-04:1, UC-05:2, UC-06:2, UC-07:2, UC-08:4 — its realization has one boundary, one
controller and three entities, UC-09:1, UC-10:2, UC-11:2, UC-12:2) plus **4 cross-cutting
dependencies** below — 28 connectors in total.
| UC | From (controller) | To | Type | Serves |
|---|---|---|---|---|
| UC-05 | TrainingService | DomainError (Architecture Overview) | Dependency «uses» | Alternate flows (error paths) |
| UC-06 | EquipmentService | DomainError (Architecture Overview) | Dependency «uses» | Alternate flows (error paths) |
| UC-08 | AdventureService | DomainError (Architecture Overview) | Dependency «uses» | Exception (unknown enemy → 500) |
| UC-12 | ReviewBoard | DomainError (Architecture Overview) | Dependency «uses» | Alternate flows (error paths) |
## 3. Design component model (joint architecture, added 2026-10-03)
Package `Analysis-Design / Design` — components of the three repos and their dependency
rule (sources: design note §2–3, mapping §2 dependency paragraph, client repo @ 34dc02f,
common repo @ wi-2-openup-docs). Every component note carries the UCs it serves.
| Component | Kind | Notes (traceability) |
|---|---|---|
| skara-brae-client | Component (presentation tier) | Vue 3.5 SPA (TypeScript, Pinia, Vite); monolithic App.vue @ 34dc02f; JWT bearer; REST via Vite proxy to :8080. Serves UC-01…12 as the player-facing boundary. |
| sb-api | Component (boundary/adapter) | Axum routes/handlers, `domain_status`, `responses` (wire-format freeze). May import sb-core, sb-shared, sb-common, sb-db, sb-auth (design §3). Serves all UC boundaries per §2. |
| sb-auth | Component (adapter) | argon2 hashing + JWT (UC-01/02). Imports sb-shared. |
| sb-db | Component (adapter) | persistence via `sb-db::Database`. Imports sb-shared. |
| sb-core | Component (domain) | controllers/entities: adventure (UC-08), services (UC-05/06), character (UC-03/12), combat, spells (UC-11), songs (UC-10), data/items (UC-04). Must not reference axum/http/serde_json::json!/tokio/sb-db/sb-api (design §3). |
| sb-shared | Component (domain types) | `User`, `Character`, `Item`, response types. Zero HTTP imports. Entities of all UCs. |
| sb-common | Component (data) | bilingual data package: data/ JSON (items, enemies, spells, zones), rust/ crate, ts/ package, codegen/. UC-04 data source; consumed by sb-core + sb-api; future client codegen. |
Dependencies (arrows = depends on): skara-brae-client → sb-api (REST/JSON);
sb-api → {sb-core, sb-shared, sb-db, sb-auth, sb-common}; sb-core → {sb-shared, sb-common};
sb-db → sb-shared; sb-auth → sb-shared. The dependency rule: inward toward
sb-core/sb-shared (domain); adapters (sb-api/sb-db/sb-auth) and the client at the edge.
## 4. Research summary — keyfob-station EA pipeline (reference)
Source: read-only clone `http://192.168.1.10:3000/jan/keyfob-station.git` (inspected 2026-10-03; no modifications). Source: read-only clone `http://192.168.1.10:3000/jan/keyfob-station.git` (inspected 2026-10-03; no modifications).

View file

@ -201,3 +201,42 @@ UC-07:3, UC-08:5, UC-09:2, UC-10:3, UC-11:3, UC-12:3 — total 36.
content — it was generated from the mapping markdown). content — it was generated from the mapping markdown).
2. Save the model; commit `.eapx` + `exports/` + `scripts/openup/` together as one commit 2. Save the model; commit `.eapx` + `exports/` + `scripts/openup/` together as one commit
`openup: EA model WI-2 baseline` in **skara-brae-common**. `openup: EA model WI-2 baseline` in **skara-brae-common**.
---
## 6. Enrichment (2026-10-03, after manual layout session) — SURGICAL only
The user laid out diagrams by hand in EA after the WI-2 baseline build. From here on the
model may only be touched by the two surgical scripts below — **never** re-run
`build-model.ps1` (it deletes and rebuilds the Requirements / Analysis-Design packages and
would destroy manual layout). Sources: `uc-realization-mapping.md` §2a (interactions) and
§3 (design component model).
### 6.1 Analysis associations — `scripts/openup/add-analysis-associations.ps1`
- Inside each `UC-NN Realization` package: Association connectors per mapping §2a
(boundary→controller, controller→entity; UC-04 boundary→entity) — **24 associations**
(UC-01:2, UC-02:1, UC-03:3, UC-04:1, UC-05:2, UC-06:2, UC-07:2, UC-08:4, UC-09:1,
UC-10:2, UC-11:2, UC-12:2).
- 4 Dependencies `«uses»` from TrainingService, EquipmentService, AdventureService,
ReviewBoard to `DomainError` (Architecture Overview) — the error paths of UC-05/06/08/12.
- DiagramLinks added to each package's existing class diagram so the connectors render
WITHOUT touching any DiagramObject (manual layout preserved). Idempotent: existing
connectors are skipped.
### 6.2 Design package — `scripts/openup/add-design-package.ps1`
- Creates `Analysis-Design / Design` (rebuilds only this package — agent-owned, contains
no manual edits): 7 components (skara-brae-client, sb-api, sb-auth, sb-db, sb-core,
sb-shared, sb-common) with traceability notes, 10 Dependency connectors, one Component
diagram `Component Architecture` (layered layout, dependency rule inward).
### 6.3 Verification
1. 24 associations across realization packages (per-UC counts above); 4 `«uses»`
dependencies to DomainError; still 0 `«include»` connectors.
2. `Design` package: 7 components, 10 dependencies, 1 diagram.
3. Baseline intact: 3 actors, 12 use cases, 12 realization packages, 13 actor-UC
associations; Requirements tree untouched.
4. Manual layout preserved: the scripts add no/modify no DiagramObjects in existing
diagrams (only the new `Design` diagram carries its own object layout).

View file

@ -1,295 +1,271 @@
{ {
"exportedFrom": "docs/openup/analysis-design/uc-realization-mapping.md §2, generated by scripts/openup/markdown-to-json.py — generated file, never hand-edit", "exportedFrom": "docs/openup/model/skara-brae.eapx (Analysis-Design realization packages)",
"realizations": [ "realizations": [
{ {
"id": "UC-01 Realization", "id": "UC-01 Realization",
"elements": [ "useCase": "UC-01 Register Account",
{ "elements": [
"name": "AccountRegistry", {
"stereotype": "controller", "name": "AccountRegistry",
"designElement": "`sb-api::routes::{register, seed_users}`", "stereotype": "controller",
"notes": "argon2 hash via `sb-auth::hash`; JWT via `sb-auth::jwt`" "notes": "Design element: . Design element: `sb-api::routes::{register, seed_users}`. argon2 hash via `sb-auth::hash`; JWT via `sb-auth::jwt`"
}, },
{ {
"name": "UserAccount", "name": "AuthBoundary",
"stereotype": "entity", "stereotype": "boundary",
"designElement": "`sb-shared::user::User`, `Role`", "notes": "Design element: . Design element: `POST /api/auth/register`, `AuthResponse`. "
"notes": "persisted via `sb-db::Database`" },
}, {
{ "name": "UserAccount",
"name": "AuthBoundary", "stereotype": "entity",
"stereotype": "boundary", "notes": "Design element: . Design element: `sb-shared::user::User`, `Role`. persisted via `sb-db::Database`"
"designElement": "`POST /api/auth/register`, `AuthResponse`", }
"notes": "" ]
} },
], {
"useCase": "Register Account" "id": "UC-02 Realization",
}, "useCase": "UC-02 Authenticate",
{ "elements": [
"id": "UC-02 Realization", {
"elements": [ "name": "Authenticator",
{ "stereotype": "controller",
"name": "Authenticator", "notes": "Design element: . Design element: `sb-api::routes::login`, `sb-auth::{hash::verify_password, jwt::create_token}`. "
"stereotype": "controller", },
"designElement": "`sb-api::routes::login`, `sb-auth::{hash::verify_password, jwt::create_token}`", {
"notes": "" "name": "TokenService",
}, "stereotype": "boundary",
{ "notes": "Design element: . Design element: `POST /api/auth/login`, `AuthResponse`, `middleware::auth::auth_middleware`. middleware guards all UC-03…12"
"name": "TokenService", }
"stereotype": "boundary", ]
"designElement": "`POST /api/auth/login`, `AuthResponse`, `middleware::auth::auth_middleware`", },
"notes": "middleware guards all UC-03…12" {
} "id": "UC-03 Realization",
], "useCase": "UC-03 Manage Characters",
"useCase": "Authenticate" "elements": [
}, {
{ "name": "Character",
"id": "UC-03 Realization", "stereotype": "entity",
"elements": [ "notes": "Design element: . Design element: `sb-shared::character::Character` (+ `Race`, `Class`, `CharacterStats`). "
{ },
"name": "CharacterFactory", {
"stereotype": "controller", "name": "CharacterCatalog",
"designElement": "`sb-core::character::create_character`", "stereotype": "boundary",
"notes": "takes injected rng (WI-1)" "notes": "Design element: . Design element: `GET/POST /api/users/{id}/characters`, `GET /api/characters/{id}`. store: `AppState.characters`"
}, },
{ {
"name": "Character", "name": "CharacterFactory",
"stereotype": "entity", "stereotype": "controller",
"designElement": "`sb-shared::character::Character` (+ `Race`, `Class`, `CharacterStats`)", "notes": "Design element: . Design element: `sb-core::character::create_character`. takes injected rng (WI-1)"
"notes": "" },
}, {
{ "name": "ResourceRegenerator",
"name": "CharacterCatalog", "stereotype": "entity",
"stereotype": "boundary", "notes": "Design element: . Design element: `sb-core::character::apply_regen` (+ regen constants — WI-2 names them). included sub-flow"
"designElement": "`GET/POST /api/users/{id}/characters`, `GET /api/characters/{id}`", }
"notes": "store: `AppState.characters`" ]
}, },
{ {
"name": "ResourceRegenerator", "id": "UC-04 Realization",
"stereotype": "entity", "useCase": "UC-04 Browse Game Catalogue",
"designElement": "`sb-core::character::apply_regen` (+ regen constants — WI-2 names them)", "elements": [
"notes": "included sub-flow" {
} "name": "CatalogueBrowser",
], "stereotype": "boundary",
"useCase": "Manage Characters" "notes": "Design element: . Design element: `GET /api/{races,classes,items,enemies,adventures}`; response types `sb-shared::responses`. data from `sb-common` JSON via `sb-core::{character::RACES/CLASSES, data, items}`"
}, },
{ {
"id": "UC-04 Realization", "name": "GameData",
"elements": [ "stereotype": "entity",
{ "notes": "Design element: . Design element: `sb-core::data::{EnemyDef, ZoneDef, EncounterEntry, LootDrop}`, `items::load_all_items`. "
"name": "CatalogueBrowser", }
"stereotype": "boundary", ]
"designElement": "`GET /api/{races,classes,items,enemies,adventures}`; response types `sb-shared::responses`", },
"notes": "data from `sb-common` JSON via `sb-core::{character::RACES/CLASSES, data, items}`" {
}, "id": "UC-05 Realization",
{ "useCase": "UC-05 Train Stat",
"name": "GameData", "elements": [
"stereotype": "entity", {
"designElement": "`sb-core::data::{EnemyDef, ZoneDef, EncounterEntry, LootDrop}`, `items::load_all_items`", "name": "GymBoundary",
"notes": "" "stereotype": "boundary",
} "notes": "Design element: . Design element: `GET/POST /api/characters/{id}/gym`, `GymResponse`, `TrainStatResponse`. "
], },
"useCase": "Browse Game Catalogue" {
}, "name": "TrainingGrounds",
{ "stereotype": "entity",
"id": "UC-05 Realization", "notes": "Design element: . Design element: `sb-core::character::TRAINING_GROUNDS`, `training_cost` (+ cost constants WI-2). "
"elements": [ },
{ {
"name": "TrainingService", "name": "TrainingService",
"stereotype": "controller", "stereotype": "controller",
"designElement": "`sb-core::services::train_stat`", "notes": "Design element: . Design element: `sb-core::services::train_stat`. error type `DomainError` (WI-2)"
"notes": "error type `DomainError` (WI-2)" }
}, ]
{ },
"name": "TrainingGrounds", {
"stereotype": "entity", "id": "UC-06 Realization",
"designElement": "`sb-core::character::TRAINING_GROUNDS`, `training_cost` (+ cost constants WI-2)", "useCase": "UC-06 Manage Equipment",
"notes": "" "elements": [
}, {
{ "name": "Equipment/Inventory",
"name": "GymBoundary", "stereotype": "entity",
"stereotype": "boundary", "notes": "Design element: . Design element: `Character.equipment` / `.inventory`, `sb-shared::item::Item`. "
"designElement": "`GET/POST /api/characters/{id}/gym`, `GymResponse`, `TrainStatResponse`", },
"notes": "" {
} "name": "EquipmentBoundary",
], "stereotype": "boundary",
"useCase": "Train Stat" "notes": "Design element: . Design element: `POST /api/characters/{id}/{equip,unequip}`. "
}, },
{ {
"id": "UC-06 Realization", "name": "EquipmentService",
"elements": [ "stereotype": "controller",
{ "notes": "Design element: . Design element: `sb-core::services::{equip_item, unequip_item}`. `DomainError` (WI-2)"
"name": "EquipmentService", }
"stereotype": "controller", ]
"designElement": "`sb-core::services::{equip_item, unequip_item}`", },
"notes": "`DomainError` (WI-2)" {
}, "id": "UC-07 Realization",
{ "useCase": "UC-07 Trade at Shop",
"name": "Equipment/Inventory", "elements": [
"stereotype": "entity", {
"designElement": "`Character.equipment` / `.inventory`, `sb-shared::item::Item`", "name": "ShopBoundary",
"notes": "" "stereotype": "boundary",
}, "notes": "Design element: . Design element: `GET/POST /api/characters/{id}/shop`, `ShopResponse`, `ShopActionResponse`. "
{ },
"name": "EquipmentBoundary", {
"stereotype": "boundary", "name": "ShopService",
"designElement": "`POST /api/characters/{id}/{equip,unequip}`", "stereotype": "controller",
"notes": "" "notes": "Design element: . Design element: `sb-api::routes::{shop_action, buy_item, sell_item}` (handler-side; extraction candidate, design note Q3). prices: `sb-core::items::{item_price, item_sell_price}`"
} },
], {
"useCase": "Manage Equipment" "name": "ShopStock",
}, "stereotype": "entity",
{ "notes": "Design element: . Design element: `AppState.shop_stock`. persisted via `sb-db`"
"id": "UC-07 Realization", }
"elements": [ ]
{ },
"name": "ShopService", {
"stereotype": "controller", "id": "UC-08 Realization",
"designElement": "`sb-api::routes::{shop_action, buy_item, sell_item}` (handler-side; extraction candidate, design note Q3)", "useCase": "UC-08 Run Adventure",
"notes": "prices: `sb-core::items::{item_price, item_sell_price}`" "elements": [
}, {
{ "name": "AdventureBoundary",
"name": "ShopStock", "stereotype": "boundary",
"stereotype": "entity", "notes": "Design element: . Design element: `GET /api/characters/{id}/adventure`; `AdventureRunResponse` (**new, WI-2**), `sb-shared::responses::CombatLog`. "
"designElement": "`AppState.shop_stock`", },
"notes": "persisted via `sb-db`" {
}, "name": "AdventureService",
{ "stereotype": "controller",
"name": "ShopBoundary", "notes": "Design element: . Design element: `sb-core::adventure::run_adventure` (**new, WI-2**) + `services::pick_enemy` → `Result` (WI-2). takes injected rng; `DomainError` (WI-2)"
"stereotype": "boundary", },
"designElement": "`GET/POST /api/characters/{id}/shop`, `ShopResponse`, `ShopActionResponse`", {
"notes": "" "name": "Combatant",
} "stereotype": "entity",
], "notes": "Design element: . Design element: `sb-core::combat::CombatStats`. built from Character/EnemyDef"
"useCase": "Trade at Shop" },
}, {
{ "name": "CombatEngine",
"id": "UC-08 Realization", "stereotype": "entity",
"elements": [ "notes": "Design element: . Design element: `sb-core::combat::{resolve_combat_full, resolve_attack, crit tables, open_ended_roll}` + combat constants (WI-2). "
{ },
"name": "AdventureService", {
"stereotype": "controller", "name": "SpellEffect / BardSong",
"designElement": "`sb-core::adventure::run_adventure` (**new, WI-2**) + `services::pick_enemy` → `Result` (WI-2)", "stereotype": "entity",
"notes": "takes injected rng; `DomainError` (WI-2)" "notes": "Design element: . Design element: `sb-core::combat::SpellEffect`, `sb-core::spells`, `sb-core::songs`. "
}, }
{ ]
"name": "CombatEngine", },
"stereotype": "entity", {
"designElement": "`sb-core::combat::{resolve_combat_full, resolve_attack, crit tables, open_ended_roll}` + combat constants (WI-2)", "id": "UC-09 Realization",
"notes": "" "useCase": "UC-09 Heal at Temple",
}, "elements": [
{ {
"name": "Combatant", "name": "HealingService",
"stereotype": "entity", "stereotype": "controller",
"designElement": "`sb-core::combat::CombatStats`", "notes": "Design element: . Design element: `sb-api::routes::heal_character` (handler-side; extraction candidate, Q3). "
"notes": "built from Character/EnemyDef" },
}, {
{ "name": "TempleBoundary",
"name": "SpellEffect / BardSong", "stereotype": "boundary",
"stereotype": "entity", "notes": "Design element: . Design element: `POST /api/characters/{id}/heal`; `HealResponse` (**new, WI-2**). "
"designElement": "`sb-core::combat::SpellEffect`, `sb-core::spells`, `sb-core::songs`", }
"notes": "" ]
}, },
{ {
"name": "AdventureBoundary", "id": "UC-10 Realization",
"stereotype": "boundary", "useCase": "UC-10 Practice Bard Song",
"designElement": "`GET /api/characters/{id}/adventure`; `AdventureRunResponse` (**new, WI-2**), `sb-shared::responses::CombatLog`", "elements": [
"notes": "" {
} "name": "BardSong",
], "stereotype": "entity",
"useCase": "Run Adventure" "notes": "Design element: . Design element: `sb-core::songs::BARD_SONGS`, `Character.active_song`. "
}, },
{ {
"id": "UC-09 Realization", "name": "SongBoundary",
"elements": [ "stereotype": "boundary",
{ "notes": "Design element: . Design element: `GET/POST /api/characters/{id}/songs`, `SongsResponse`, `SetSongResponse`. "
"name": "HealingService", },
"stereotype": "controller", {
"designElement": "`sb-api::routes::heal_character` (handler-side; extraction candidate, Q3)", "name": "SongService",
"notes": "" "stereotype": "controller",
}, "notes": "Design element: . Design element: `sb-api::routes::set_song`; `sb-core::songs::available_songs`. "
{ }
"name": "TempleBoundary", ]
"stereotype": "boundary", },
"designElement": "`POST /api/characters/{id}/heal`; `HealResponse` (**new, WI-2**)", {
"notes": "" "id": "UC-11 Realization",
} "useCase": "UC-11 Study Magic",
], "elements": [
"useCase": "Heal at Temple" {
}, "name": "MagicBoundary",
{ "stereotype": "boundary",
"id": "UC-10 Realization", "notes": "Design element: . Design element: `GET/POST /api/characters/{id}/spells`; `SpellbookResponse`, `LearnSpellResponse` (**new, WI-2**). "
"elements": [ },
{ {
"name": "SongService", "name": "MagicService",
"stereotype": "controller", "stereotype": "controller",
"designElement": "`sb-api::routes::set_song`; `sb-core::songs::available_songs`", "notes": "Design element: . Design element: `sb-api::routes::{get_spells, learn_spell_level}`; `sb-core::spells::{spells_for_class, spell_learn_cost, max_spell_points, caster_spell_level (WI-2)}`. "
"notes": "" },
}, {
{ "name": "Spell",
"name": "BardSong", "stereotype": "entity",
"stereotype": "entity", "notes": "Design element: . Design element: `sb-core::spells::SpellDef`, `Character.{known_spells, active_spell, spell_points}`. "
"designElement": "`sb-core::songs::BARD_SONGS`, `Character.active_song`", }
"notes": "" ]
}, },
{ {
"name": "SongBoundary", "id": "UC-12 Realization",
"stereotype": "boundary", "useCase": "UC-12 Level Up at Review Board",
"designElement": "`GET/POST /api/characters/{id}/songs`, `SongsResponse`, `SetSongResponse`", "elements": [
"notes": "" {
} "name": "Progression",
], "stereotype": "entity",
"useCase": "Practice Bard Song" "notes": "Design element: . Design element: `sb-core::character::{xp_for_level, level_up_cost, MAX_LEVEL}` (+ curve constants WI-2). "
}, },
{ {
"id": "UC-11 Realization", "name": "ReviewBoard",
"elements": [ "stereotype": "controller",
{ "notes": "Design element: . Design element: `sb-core::character::level_up`. `DomainError` (WI-2)"
"name": "MagicService", },
"stereotype": "controller", {
"designElement": "`sb-api::routes::{get_spells, learn_spell_level}`; `sb-core::spells::{spells_for_class, spell_learn_cost, max_spell_points, caster_spell_level (WI-2)}`", "name": "ReviewBoundary",
"notes": "" "stereotype": "boundary",
}, "notes": "Design element: . Design element: `POST /api/characters/{id}/level-up`, `LevelUpResponse`. "
{ }
"name": "Spell", ]
"stereotype": "entity", }
"designElement": "`sb-core::spells::SpellDef`, `Character.{known_spells, active_spell, spell_points}`", ],
"notes": "" "crossCutting": [
}, {
{ "name": "Constants",
"name": "MagicBoundary", "notes": "sb-core::constants realizes NFR \"no magic numbers\"."
"stereotype": "boundary", },
"designElement": "`GET/POST /api/characters/{id}/spells`; `SpellbookResponse`, `LearnSpellResponse` (**new, WI-2**)", {
"notes": "" "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."
], },
"useCase": "Study Magic" {
}, "name": "Responses",
{ "notes": "sb-api::routes::responses realizes NFR-19 (wire-format freeze)."
"id": "UC-12 Realization", }
"elements": [ ]
{ }
"name": "ReviewBoard",
"stereotype": "controller",
"designElement": "`sb-core::character::level_up`",
"notes": "`DomainError` (WI-2)"
},
{
"name": "Progression",
"stereotype": "entity",
"designElement": "`sb-core::character::{xp_for_level, level_up_cost, MAX_LEVEL}` (+ curve constants WI-2)",
"notes": ""
},
{
"name": "ReviewBoundary",
"stereotype": "boundary",
"designElement": "`POST /api/characters/{id}/level-up`, `LevelUpResponse`",
"notes": ""
}
],
"useCase": "Level Up at Review Board"
}
],
"crossCutting": "Cross-cutting design elements (WI-2): `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; `sb-core::constants` realizes NFR \"no magic numbers\"; `sb-api::routes::responses` realizes NFR-19 (wire-format freeze).",
"dependencyRule": "Dependency rule check (Clean Architecture): all controllers/entities above 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)."
}

View file

@ -0,0 +1,546 @@
{
"exportedFrom": "docs/openup/model/skara-brae.eapx (Requirements/Use Case Model)",
"actors": [
{
"name": "Administrator",
"notes": "Player with elevated rights (role = admin); may view any user\u0027s and any character\u0027s resources read-only."
},
{
"name": "Guest",
"notes": "Unauthenticated caller; only register/login are open."
},
{
"name": "Player",
"notes": "Authenticated user owning one or more characters (JWT role player)."
}
],
"useCases": [
{
"name": "UC-01 Register Account",
"notes": "Open registration. No precondition.",
"actors": [
"Guest"
],
"scenarios": [
{
"name": "Main Success Scenario",
"type": "Basic Path",
"notes": "",
"steps": [
{
"actor": 0,
"text": "Guest submits name (3-32 chars) and password (\u003e= 6 chars)."
},
{
"actor": 0,
"text": "System verifies the name is unused -\u003e 409 CONFLICT if taken."
},
{
"actor": 0,
"text": "System hashes the password (argon2), assigns the player role and the next id, persists the user."
},
{
"actor": 0,
"text": "System issues a JWT (HS256, player role) and returns {token, user_id, name, role}."
}
]
},
{
"name": "Alternate flows",
"type": "Alternate",
"notes": "name length invalid -\u003e 400; password too short -\u003e 400; name taken -\u003e 409; hashing/token failure -\u003e 500.",
"steps": [
]
}
]
},
{
"name": "UC-02 Authenticate",
"notes": "Login with name and password; issues a JWT with the user\u0027s real role.",
"actors": [
"Guest"
],
"scenarios": [
{
"name": "Main Success Scenario",
"type": "Basic Path",
"notes": "",
"steps": [
{
"actor": 0,
"text": "Guest submits name and password."
},
{
"actor": 0,
"text": "System finds the user (401 if unknown) and verifies the password hash (401 on mismatch)."
},
{
"actor": 0,
"text": "System issues a JWT carrying the user\u0027s real role (admin/player) and returns {token, user_id, name, role}."
}
]
},
{
"name": "Alternate flows",
"type": "Alternate",
"notes": "unknown name or wrong password -\u003e 401 UNAUTHORIZED; token creation failure -\u003e 500.",
"steps": [
]
}
]
},
{
"name": "UC-03 Manage Characters",
"notes": "Create, list and view characters. Admin may act for any user (read-only view).",
"actors": [
"Player",
"Administrator"
],
"scenarios": [
{
"name": "Main Success Scenario (create)",
"type": "Basic Path",
"notes": "Admin may act for any user.",
"steps": [
{
"actor": 0,
"text": "Player submits character name, race, class for their user id."
},
{
"actor": 0,
"text": "System validates the target user exists (404) and the trimmed name is 1-32 chars (400)."
},
{
"actor": 0,
"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": 0,
"text": "System assigns the next character id, stores and persists the character; returns it fully."
}
]
},
{
"name": "list/view",
"type": "Alternate",
"notes": "Player lists their characters (Admin: any user\u0027s); 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 -\u003e 403 FORBIDDEN; user/character not found -\u003e 404; invalid name -\u003e 400.",
"steps": [
]
}
]
},
{
"name": "UC-04 Browse Game Catalogue",
"notes": "Races, classes, items (type/quality/slot filters), enemies, adventure zones from static game data.",
"actors": [
"Player"
],
"scenarios": [
{
"name": "Main Success Scenario",
"type": "Basic Path",
"notes": "",
"steps": [
{
"actor": 0,
"text": "Player requests the catalogue: races, classes, items (optional type/quality/slot filters), enemies, or adventure zones."
},
{
"actor": 0,
"text": "System returns static game data loaded from skara-brae-common JSON (class info is enriched with is_caster)."
}
]
},
{
"name": "Alternate flows",
"type": "Alternate",
"notes": "single item by unknown id -\u003e 404; filters may return an empty list (not an error).",
"steps": [
]
}
]
},
{
"name": "UC-05 Train Stat",
"notes": "Train a stat at a training ground; costs 1 energy + gold. Includes Regenerate Resources.",
"actors": [
"Player"
],
"scenarios": [
{
"name": "Main Success Scenario",
"type": "Basic Path",
"notes": "Precondition: character exists and caller may act on it.",
"steps": [
{
"actor": 0,
"text": "Player selects a training ground (strength / dexterity / constitution / intelligence / luck) and submits the stat name."
},
{
"actor": 0,
"text": "System applies Regenerate Resources."
},
{
"actor": 0,
"text": "System checks energy \u003e= 1 and gold \u003e= training cost (cost grows ~1.3x per 10 stat points)."
},
{
"actor": 0,
"text": "System deducts 1 energy and the gold cost, increments the stat; constitution training recomputes max HP (current HP clamped)."
},
{
"actor": 0,
"text": "System persists and returns {success, message, character}."
}
]
},
{
"name": "Alternate flows",
"type": "Alternate",
"notes": "foreign character -\u003e 403; character not found -\u003e 404; not enough energy -\u003e 422; not enough gold -\u003e 422; unknown stat name -\u003e 400.",
"steps": [
]
}
]
},
{
"name": "UC-06 Manage Equipment",
"notes": "Equip from inventory / unequip back to inventory.",
"actors": [
"Player"
],
"scenarios": [
{
"name": "Main Success Scenario (equip)",
"type": "Basic Path",
"notes": "",
"steps": [
{
"actor": 0,
"text": "Player submits an item id from their inventory."
},
{
"actor": 0,
"text": "System moves the item from inventory to its slot; a previously equipped item in that slot returns to inventory."
},
{
"actor": 0,
"text": "System persists and returns the updated character."
}
]
},
{
"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 -\u003e 403; character/item not found -\u003e 404; item not in inventory -\u003e 400; nothing equipped in slot -\u003e 400.",
"steps": [
]
}
]
},
{
"name": "UC-07 Trade at Shop",
"notes": "Buy from (always-stocked + rotating) shop stock or sell inventory items.",
"actors": [
"Player"
],
"scenarios": [
{
"name": "Main Success Scenario (buy)",
"type": "Basic Path",
"notes": "",
"steps": [
{
"actor": 0,
"text": "Player views the shop: always-stocked items plus rotating stock (with quantities) alongside their inventory."
},
{
"actor": 0,
"text": "Player submits buy + item id."
},
{
"actor": 0,
"text": "System checks gold \u003e= price (422), and for non-always-in-stock items decrements the limited stock (410 GONE if 0)."
},
{
"actor": 0,
"text": "System deducts gold, adds the item to inventory, persists character and stock; returns {success, message, gold}."
}
]
},
{
"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) -\u003e 400; item not found -\u003e 404; insufficient gold -\u003e 422; stock exhausted -\u003e 410.",
"steps": [
]
}
]
},
{
"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 -\u003e 500 (post WI-2; previously a panic). WI-2 design driver (sb_core::adventure extraction).",
"actors": [
"Player"
],
"scenarios": [
{
"name": "Main Success Scenario",
"type": "Basic Path",
"notes": "Precondition: character exists and caller may act on it.",
"steps": [
{
"actor": 0,
"text": "Player requests an adventure for a zone (default skara_brae_streets)."
},
{
"actor": 0,
"text": "System validates the zone (404) and applies Regenerate Resources."
},
{
"actor": 0,
"text": "System refuses incapacitated characters (HP \u003c= 0 -\u003e 400) and characters with nerve below the zone cost (422); otherwise deducts the nerve cost."
},
{
"actor": 0,
"text": "System draws one enemy from the zone\u0027s weighted encounter table."
},
{
"actor": 0,
"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": 0,
"text": "On player victory: XP and gold per the enemy definition, character HP set to the last round\u0027s defender HP, and loot dropped per-item chance rolls (only items existing in the catalogue)."
},
{
"actor": 0,
"text": "On defeat: character HP set to 0; no rewards."
},
{
"actor": 0,
"text": "Spell-point upkeep is charged whenever a spell was active, win or lose."
},
{
"actor": 0,
"text": "System persists the character and returns {zone, enemy, combat_log, loot} (loot = item display names)."
}
]
},
{
"name": "Alternate flows",
"type": "Alternate",
"notes": "foreign character -\u003e 403; character/zone not found -\u003e 404; incapacitated -\u003e 400; insufficient nerve -\u003e 422.",
"steps": [
]
},
{
"name": "Exception",
"type": "Exception",
"notes": "encounter references an unknown enemy (data error) -\u003e 500 (post WI-2; previously a panic).",
"steps": [
]
}
]
},
{
"name": "UC-09 Heal at Temple",
"notes": "Full or partial healing, cost = amount healed, minimum 1 gold.",
"actors": [
"Player"
],
"scenarios": [
{
"name": "Main Success Scenario",
"type": "Basic Path",
"notes": "",
"steps": [
{
"actor": 0,
"text": "Player requests healing, optionally partial."
},
{
"actor": 0,
"text": "System computes missing HP; full heals all, partial heals half (rounded up); cost = amount healed, minimum 1 gold."
},
{
"actor": 0,
"text": "System checks gold (422), deducts it, restores HP, persists; returns {hp, max_hp, gold, healed: true}."
}
]
},
{
"name": "Alternate flows",
"type": "Alternate",
"notes": "foreign character -\u003e 403; not found -\u003e 404; already at full HP -\u003e 400; insufficient gold -\u003e 422.",
"steps": [
]
}
]
},
{
"name": "UC-10 Practice Bard Song",
"notes": "Set or clear the active bard song (Bard only, level-gated).",
"actors": [
"Player"
],
"scenarios": [
{
"name": "Main Success Scenario",
"type": "Basic Path",
"notes": "Precondition: character class is Bard.",
"steps": [
{
"actor": 0,
"text": "Player views available songs (level-gated, with combat effect descriptions) and the active song."
},
{
"actor": 0,
"text": "Player submits a song id; system validates level requirement (422) and sets it active; returns {success, message, active_song}."
}
]
},
{
"name": "clear",
"type": "Alternate",
"notes": "submitting no song id clears the active song.",
"steps": [
]
},
{
"name": "Alternate flows",
"type": "Alternate",
"notes": "non-Bard -\u003e 403; unknown song -\u003e 404; level too low -\u003e 422; foreign character -\u003e 403; not found -\u003e 404.",
"steps": [
]
}
]
},
{
"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.",
"actors": [
"Player"
],
"scenarios": [
{
"name": "Main Success Scenario (view spellbook)",
"type": "Basic Path",
"notes": "Precondition: character belongs to a caster class (Mage-\u003emagician, Wizard, Conjurer, Sorcerer). Spell level = (character level / 3) + 1 - uncapped by code (level 100 -\u003e spell level 34); learn requests are validated to 1-7.",
"steps": [
{
"actor": 0,
"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."
}
]
},
{
"name": "learn spell level",
"type": "Alternate",
"notes": "",
"steps": [
{
"actor": 0,
"text": "Player submits a spell level (1-7)."
},
{
"actor": 0,
"text": "System validates caster class (403), level range (400), level not above the character\u0027s current spell level (422), gold \u003e= learn cost (422), and that new spells exist (400)."
},
{
"actor": 0,
"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}."
}
]
},
{
"name": "activate/clear active spell",
"type": "Alternate",
"notes": "",
"steps": [
{
"actor": 0,
"text": "Player submits a spell id (or empty to clear)."
},
{
"actor": 0,
"text": "System validates the spell is known (403), exists (404), and spell points suffice (422); deducts the spell\u0027s points and sets it active; returns {active_spell, spell_points}."
}
]
}
]
},
{
"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.",
"actors": [
"Player"
],
"scenarios": [
{
"name": "Main Success Scenario",
"type": "Basic Path",
"notes": "",
"steps": [
{
"actor": 0,
"text": "Player requests a level-up."
},
{
"actor": 0,
"text": "System validates: below max level 100, XP \u003e= 18*100*(level)^2 for the next level (i.e. xp_for_level(next)), gold \u003e= 500*next level."
},
{
"actor": 0,
"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}."
}
]
},
{
"name": "Alternate flows",
"type": "Alternate",
"notes": "already at max level; not enough XP; not enough gold -\u003e all 422. Foreign character -\u003e 403; not found -\u003e 404.",
"steps": [
]
}
]
}
]
}

Binary file not shown.

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'; @{ 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 = @() } Notes = 'already at max level; not enough XP; not enough gold -> all 422. Foreign character -> 403; not found -> 404.'; Steps = @() }
) )
) }
foreach ($useCase in $useCases) { foreach ($useCase in $useCases) {
$ucElement = $elements[$useCase.Name] $ucElement = $elements[$useCase.Name]
foreach ($scenarioData in $scenariosByUc[$useCase.Name]) { foreach ($scenarioData in $scenariosByUc[$useCase.Name]) {
@ -375,7 +375,12 @@ try {
$architecture.Elements.Refresh() $architecture.Elements.Refresh()
$step = 'UC realization packages (from exports/uc-realizations.json)' $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) { 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') $realizationPackage = $analysisDesign.Packages.AddNew($realization.id, 'Package')
$null = $realizationPackage.Update() $null = $realizationPackage.Update()
$analysisDesign.Packages.Refresh() $analysisDesign.Packages.Refresh()
@ -385,7 +390,7 @@ try {
$realizationPackage.Elements.Refresh() $realizationPackage.Elements.Refresh()
$connector = $collaboration.Connectors.AddNew('', 'Realization') $connector = $collaboration.Connectors.AddNew('', 'Realization')
$connector.ClientID = $collaboration.ElementID $connector.ClientID = $collaboration.ElementID
$connector.SupplierID = ($elements[$realization.useCase]).ElementID $connector.SupplierID = $supplierElement.ElementID
$null = $connector.Update() $null = $connector.Update()
$collaboration.Connectors.Refresh() $collaboration.Connectors.Refresh()