diff --git a/docs/openup/analysis-design/uc-realization-mapping.md b/docs/openup/analysis-design/uc-realization-mapping.md index 64913c8..1834100 100644 --- a/docs/openup/analysis-design/uc-realization-mapping.md +++ b/docs/openup/analysis-design/uc-realization-mapping.md @@ -16,10 +16,18 @@ Model: Skara Brae │ └── Supplementary Specifications (from requirements/supplementary-specifications.md) └── Analysis-Design ├── Architecture Overview (crate/layer diagram, dependency rule) - └── UC-01 Realization … UC-12 Realization (one package per UC: analysis classes, - design elements, realization table below) + ├── UC-01 Realization … UC-12 Realization (one package per UC: analysis classes, + │ 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) 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). -## 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). diff --git a/docs/openup/model/BUILD-CHECKLIST.md b/docs/openup/model/BUILD-CHECKLIST.md index a5bb195..e577f4e 100644 --- a/docs/openup/model/BUILD-CHECKLIST.md +++ b/docs/openup/model/BUILD-CHECKLIST.md @@ -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). 2. Save the model; commit `.eapx` + `exports/` + `scripts/openup/` together as one commit `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). diff --git a/docs/openup/model/exports/uc-realizations.json b/docs/openup/model/exports/uc-realizations.json index 2fef88f..c7c1c39 100644 --- a/docs/openup/model/exports/uc-realizations.json +++ b/docs/openup/model/exports/uc-realizations.json @@ -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", - "realizations": [ - { - "id": "UC-01 Realization", - "elements": [ - { - "name": "AccountRegistry", - "stereotype": "controller", - "designElement": "`sb-api::routes::{register, seed_users}`", - "notes": "argon2 hash via `sb-auth::hash`; JWT via `sb-auth::jwt`" - }, - { - "name": "UserAccount", - "stereotype": "entity", - "designElement": "`sb-shared::user::User`, `Role`", - "notes": "persisted via `sb-db::Database`" - }, - { - "name": "AuthBoundary", - "stereotype": "boundary", - "designElement": "`POST /api/auth/register`, `AuthResponse`", - "notes": "" - } - ], - "useCase": "Register Account" - }, - { - "id": "UC-02 Realization", - "elements": [ - { - "name": "Authenticator", - "stereotype": "controller", - "designElement": "`sb-api::routes::login`, `sb-auth::{hash::verify_password, jwt::create_token}`", - "notes": "" - }, - { - "name": "TokenService", - "stereotype": "boundary", - "designElement": "`POST /api/auth/login`, `AuthResponse`, `middleware::auth::auth_middleware`", - "notes": "middleware guards all UC-03…12" - } - ], - "useCase": "Authenticate" - }, - { - "id": "UC-03 Realization", - "elements": [ - { - "name": "CharacterFactory", - "stereotype": "controller", - "designElement": "`sb-core::character::create_character`", - "notes": "takes injected rng (WI-1)" - }, - { - "name": "Character", - "stereotype": "entity", - "designElement": "`sb-shared::character::Character` (+ `Race`, `Class`, `CharacterStats`)", - "notes": "" - }, - { - "name": "CharacterCatalog", - "stereotype": "boundary", - "designElement": "`GET/POST /api/users/{id}/characters`, `GET /api/characters/{id}`", - "notes": "store: `AppState.characters`" - }, - { - "name": "ResourceRegenerator", - "stereotype": "entity", - "designElement": "`sb-core::character::apply_regen` (+ regen constants — WI-2 names them)", - "notes": "included sub-flow" - } - ], - "useCase": "Manage Characters" - }, - { - "id": "UC-04 Realization", - "elements": [ - { - "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}`" - }, - { - "name": "GameData", - "stereotype": "entity", - "designElement": "`sb-core::data::{EnemyDef, ZoneDef, EncounterEntry, LootDrop}`, `items::load_all_items`", - "notes": "" - } - ], - "useCase": "Browse Game Catalogue" - }, - { - "id": "UC-05 Realization", - "elements": [ - { - "name": "TrainingService", - "stereotype": "controller", - "designElement": "`sb-core::services::train_stat`", - "notes": "error type `DomainError` (WI-2)" - }, - { - "name": "TrainingGrounds", - "stereotype": "entity", - "designElement": "`sb-core::character::TRAINING_GROUNDS`, `training_cost` (+ cost constants WI-2)", - "notes": "" - }, - { - "name": "GymBoundary", - "stereotype": "boundary", - "designElement": "`GET/POST /api/characters/{id}/gym`, `GymResponse`, `TrainStatResponse`", - "notes": "" - } - ], - "useCase": "Train Stat" - }, - { - "id": "UC-06 Realization", - "elements": [ - { - "name": "EquipmentService", - "stereotype": "controller", - "designElement": "`sb-core::services::{equip_item, unequip_item}`", - "notes": "`DomainError` (WI-2)" - }, - { - "name": "Equipment/Inventory", - "stereotype": "entity", - "designElement": "`Character.equipment` / `.inventory`, `sb-shared::item::Item`", - "notes": "" - }, - { - "name": "EquipmentBoundary", - "stereotype": "boundary", - "designElement": "`POST /api/characters/{id}/{equip,unequip}`", - "notes": "" - } - ], - "useCase": "Manage Equipment" - }, - { - "id": "UC-07 Realization", - "elements": [ - { - "name": "ShopService", - "stereotype": "controller", - "designElement": "`sb-api::routes::{shop_action, buy_item, sell_item}` (handler-side; extraction candidate, design note Q3)", - "notes": "prices: `sb-core::items::{item_price, item_sell_price}`" - }, - { - "name": "ShopStock", - "stereotype": "entity", - "designElement": "`AppState.shop_stock`", - "notes": "persisted via `sb-db`" - }, - { - "name": "ShopBoundary", - "stereotype": "boundary", - "designElement": "`GET/POST /api/characters/{id}/shop`, `ShopResponse`, `ShopActionResponse`", - "notes": "" - } - ], - "useCase": "Trade at Shop" - }, - { - "id": "UC-08 Realization", - "elements": [ - { - "name": "AdventureService", - "stereotype": "controller", - "designElement": "`sb-core::adventure::run_adventure` (**new, WI-2**) + `services::pick_enemy` → `Result` (WI-2)", - "notes": "takes injected rng; `DomainError` (WI-2)" - }, - { - "name": "CombatEngine", - "stereotype": "entity", - "designElement": "`sb-core::combat::{resolve_combat_full, resolve_attack, crit tables, open_ended_roll}` + combat constants (WI-2)", - "notes": "" - }, - { - "name": "Combatant", - "stereotype": "entity", - "designElement": "`sb-core::combat::CombatStats`", - "notes": "built from Character/EnemyDef" - }, - { - "name": "SpellEffect / BardSong", - "stereotype": "entity", - "designElement": "`sb-core::combat::SpellEffect`, `sb-core::spells`, `sb-core::songs`", - "notes": "" - }, - { - "name": "AdventureBoundary", - "stereotype": "boundary", - "designElement": "`GET /api/characters/{id}/adventure`; `AdventureRunResponse` (**new, WI-2**), `sb-shared::responses::CombatLog`", - "notes": "" - } - ], - "useCase": "Run Adventure" - }, - { - "id": "UC-09 Realization", - "elements": [ - { - "name": "HealingService", - "stereotype": "controller", - "designElement": "`sb-api::routes::heal_character` (handler-side; extraction candidate, Q3)", - "notes": "" - }, - { - "name": "TempleBoundary", - "stereotype": "boundary", - "designElement": "`POST /api/characters/{id}/heal`; `HealResponse` (**new, WI-2**)", - "notes": "" - } - ], - "useCase": "Heal at Temple" - }, - { - "id": "UC-10 Realization", - "elements": [ - { - "name": "SongService", - "stereotype": "controller", - "designElement": "`sb-api::routes::set_song`; `sb-core::songs::available_songs`", - "notes": "" - }, - { - "name": "BardSong", - "stereotype": "entity", - "designElement": "`sb-core::songs::BARD_SONGS`, `Character.active_song`", - "notes": "" - }, - { - "name": "SongBoundary", - "stereotype": "boundary", - "designElement": "`GET/POST /api/characters/{id}/songs`, `SongsResponse`, `SetSongResponse`", - "notes": "" - } - ], - "useCase": "Practice Bard Song" - }, - { - "id": "UC-11 Realization", - "elements": [ - { - "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)}`", - "notes": "" - }, - { - "name": "Spell", - "stereotype": "entity", - "designElement": "`sb-core::spells::SpellDef`, `Character.{known_spells, active_spell, spell_points}`", - "notes": "" - }, - { - "name": "MagicBoundary", - "stereotype": "boundary", - "designElement": "`GET/POST /api/characters/{id}/spells`; `SpellbookResponse`, `LearnSpellResponse` (**new, WI-2**)", - "notes": "" - } - ], - "useCase": "Study Magic" - }, - { - "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)." -} +{ + "exportedFrom": "docs/openup/model/skara-brae.eapx (Analysis-Design realization packages)", + "realizations": [ + { + "id": "UC-01 Realization", + "useCase": "UC-01 Register Account", + "elements": [ + { + "name": "AccountRegistry", + "stereotype": "controller", + "notes": "Design element: . Design element: `sb-api::routes::{register, seed_users}`. argon2 hash via `sb-auth::hash`; JWT via `sb-auth::jwt`" + }, + { + "name": "AuthBoundary", + "stereotype": "boundary", + "notes": "Design element: . Design element: `POST /api/auth/register`, `AuthResponse`. " + }, + { + "name": "UserAccount", + "stereotype": "entity", + "notes": "Design element: . Design element: `sb-shared::user::User`, `Role`. persisted via `sb-db::Database`" + } + ] + }, + { + "id": "UC-02 Realization", + "useCase": "UC-02 Authenticate", + "elements": [ + { + "name": "Authenticator", + "stereotype": "controller", + "notes": "Design element: . Design element: `sb-api::routes::login`, `sb-auth::{hash::verify_password, jwt::create_token}`. " + }, + { + "name": "TokenService", + "stereotype": "boundary", + "notes": "Design element: . Design element: `POST /api/auth/login`, `AuthResponse`, `middleware::auth::auth_middleware`. middleware guards all UC-03…12" + } + ] + }, + { + "id": "UC-03 Realization", + "useCase": "UC-03 Manage Characters", + "elements": [ + { + "name": "Character", + "stereotype": "entity", + "notes": "Design element: . Design element: `sb-shared::character::Character` (+ `Race`, `Class`, `CharacterStats`). " + }, + { + "name": "CharacterCatalog", + "stereotype": "boundary", + "notes": "Design element: . Design element: `GET/POST /api/users/{id}/characters`, `GET /api/characters/{id}`. store: `AppState.characters`" + }, + { + "name": "CharacterFactory", + "stereotype": "controller", + "notes": "Design element: . Design element: `sb-core::character::create_character`. takes injected rng (WI-1)" + }, + { + "name": "ResourceRegenerator", + "stereotype": "entity", + "notes": "Design element: . Design element: `sb-core::character::apply_regen` (+ regen constants — WI-2 names them). included sub-flow" + } + ] + }, + { + "id": "UC-04 Realization", + "useCase": "UC-04 Browse Game Catalogue", + "elements": [ + { + "name": "CatalogueBrowser", + "stereotype": "boundary", + "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}`" + }, + { + "name": "GameData", + "stereotype": "entity", + "notes": "Design element: . Design element: `sb-core::data::{EnemyDef, ZoneDef, EncounterEntry, LootDrop}`, `items::load_all_items`. " + } + ] + }, + { + "id": "UC-05 Realization", + "useCase": "UC-05 Train Stat", + "elements": [ + { + "name": "GymBoundary", + "stereotype": "boundary", + "notes": "Design element: . Design element: `GET/POST /api/characters/{id}/gym`, `GymResponse`, `TrainStatResponse`. " + }, + { + "name": "TrainingGrounds", + "stereotype": "entity", + "notes": "Design element: . Design element: `sb-core::character::TRAINING_GROUNDS`, `training_cost` (+ cost constants WI-2). " + }, + { + "name": "TrainingService", + "stereotype": "controller", + "notes": "Design element: . Design element: `sb-core::services::train_stat`. error type `DomainError` (WI-2)" + } + ] + }, + { + "id": "UC-06 Realization", + "useCase": "UC-06 Manage Equipment", + "elements": [ + { + "name": "Equipment/Inventory", + "stereotype": "entity", + "notes": "Design element: . Design element: `Character.equipment` / `.inventory`, `sb-shared::item::Item`. " + }, + { + "name": "EquipmentBoundary", + "stereotype": "boundary", + "notes": "Design element: . Design element: `POST /api/characters/{id}/{equip,unequip}`. " + }, + { + "name": "EquipmentService", + "stereotype": "controller", + "notes": "Design element: . Design element: `sb-core::services::{equip_item, unequip_item}`. `DomainError` (WI-2)" + } + ] + }, + { + "id": "UC-07 Realization", + "useCase": "UC-07 Trade at Shop", + "elements": [ + { + "name": "ShopBoundary", + "stereotype": "boundary", + "notes": "Design element: . Design element: `GET/POST /api/characters/{id}/shop`, `ShopResponse`, `ShopActionResponse`. " + }, + { + "name": "ShopService", + "stereotype": "controller", + "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}`" + }, + { + "name": "ShopStock", + "stereotype": "entity", + "notes": "Design element: . Design element: `AppState.shop_stock`. persisted via `sb-db`" + } + ] + }, + { + "id": "UC-08 Realization", + "useCase": "UC-08 Run Adventure", + "elements": [ + { + "name": "AdventureBoundary", + "stereotype": "boundary", + "notes": "Design element: . Design element: `GET /api/characters/{id}/adventure`; `AdventureRunResponse` (**new, WI-2**), `sb-shared::responses::CombatLog`. " + }, + { + "name": "AdventureService", + "stereotype": "controller", + "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)" + }, + { + "name": "Combatant", + "stereotype": "entity", + "notes": "Design element: . Design element: `sb-core::combat::CombatStats`. built from Character/EnemyDef" + }, + { + "name": "CombatEngine", + "stereotype": "entity", + "notes": "Design element: . Design element: `sb-core::combat::{resolve_combat_full, resolve_attack, crit tables, open_ended_roll}` + combat constants (WI-2). " + }, + { + "name": "SpellEffect / BardSong", + "stereotype": "entity", + "notes": "Design element: . Design element: `sb-core::combat::SpellEffect`, `sb-core::spells`, `sb-core::songs`. " + } + ] + }, + { + "id": "UC-09 Realization", + "useCase": "UC-09 Heal at Temple", + "elements": [ + { + "name": "HealingService", + "stereotype": "controller", + "notes": "Design element: . Design element: `sb-api::routes::heal_character` (handler-side; extraction candidate, Q3). " + }, + { + "name": "TempleBoundary", + "stereotype": "boundary", + "notes": "Design element: . Design element: `POST /api/characters/{id}/heal`; `HealResponse` (**new, WI-2**). " + } + ] + }, + { + "id": "UC-10 Realization", + "useCase": "UC-10 Practice Bard Song", + "elements": [ + { + "name": "BardSong", + "stereotype": "entity", + "notes": "Design element: . Design element: `sb-core::songs::BARD_SONGS`, `Character.active_song`. " + }, + { + "name": "SongBoundary", + "stereotype": "boundary", + "notes": "Design element: . Design element: `GET/POST /api/characters/{id}/songs`, `SongsResponse`, `SetSongResponse`. " + }, + { + "name": "SongService", + "stereotype": "controller", + "notes": "Design element: . Design element: `sb-api::routes::set_song`; `sb-core::songs::available_songs`. " + } + ] + }, + { + "id": "UC-11 Realization", + "useCase": "UC-11 Study Magic", + "elements": [ + { + "name": "MagicBoundary", + "stereotype": "boundary", + "notes": "Design element: . Design element: `GET/POST /api/characters/{id}/spells`; `SpellbookResponse`, `LearnSpellResponse` (**new, WI-2**). " + }, + { + "name": "MagicService", + "stereotype": "controller", + "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)}`. " + }, + { + "name": "Spell", + "stereotype": "entity", + "notes": "Design element: . Design element: `sb-core::spells::SpellDef`, `Character.{known_spells, active_spell, spell_points}`. " + } + ] + }, + { + "id": "UC-12 Realization", + "useCase": "UC-12 Level Up at Review Board", + "elements": [ + { + "name": "Progression", + "stereotype": "entity", + "notes": "Design element: . Design element: `sb-core::character::{xp_for_level, level_up_cost, MAX_LEVEL}` (+ curve constants WI-2). " + }, + { + "name": "ReviewBoard", + "stereotype": "controller", + "notes": "Design element: . Design element: `sb-core::character::level_up`. `DomainError` (WI-2)" + }, + { + "name": "ReviewBoundary", + "stereotype": "boundary", + "notes": "Design element: . Design element: `POST /api/characters/{id}/level-up`, `LevelUpResponse`. " + } + ] + } + ], + "crossCutting": [ + { + "name": "Constants", + "notes": "sb-core::constants realizes NFR \"no magic numbers\"." + }, + { + "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": "Responses", + "notes": "sb-api::routes::responses realizes NFR-19 (wire-format freeze)." + } + ] +} \ No newline at end of file diff --git a/docs/openup/model/exports/use-case-model.json b/docs/openup/model/exports/use-case-model.json new file mode 100644 index 0000000..58931c6 --- /dev/null +++ b/docs/openup/model/exports/use-case-model.json @@ -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": [ + + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/openup/model/skara-brae.eapx b/docs/openup/model/skara-brae.eapx new file mode 100644 index 0000000..40f35ad Binary files /dev/null and b/docs/openup/model/skara-brae.eapx differ diff --git a/scripts/openup/add-analysis-associations.ps1 b/scripts/openup/add-analysis-associations.ps1 new file mode 100644 index 0000000..2a7401f --- /dev/null +++ b/scripts/openup/add-analysis-associations.ps1 @@ -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 } +} diff --git a/scripts/openup/add-design-package.ps1 b/scripts/openup/add-design-package.ps1 new file mode 100644 index 0000000..65a5ec4 --- /dev/null +++ b/scripts/openup/add-design-package.ps1 @@ -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 } +} diff --git a/scripts/openup/build-model.ps1 b/scripts/openup/build-model.ps1 index e3b09bf..cf292f2 100644 --- a/scripts/openup/build-model.ps1 +++ b/scripts/openup/build-model.ps1 @@ -305,7 +305,7 @@ try { @{ Name = 'Alternate flows'; Type = 'Alternate'; Notes = 'already at max level; not enough XP; not enough gold -> all 422. Foreign character -> 403; not found -> 404.'; Steps = @() } ) - ) + } foreach ($useCase in $useCases) { $ucElement = $elements[$useCase.Name] foreach ($scenarioData in $scenariosByUc[$useCase.Name]) { @@ -375,7 +375,12 @@ try { $architecture.Elements.Refresh() $step = 'UC realization packages (from exports/uc-realizations.json)' + $elementsByShortName = @{} + foreach ($useCase in $useCases) { $elementsByShortName[$useCase.Name.Substring(6)] = $elements[$useCase.Name] } foreach ($realization in $realizations.realizations) { + $supplierElement = $elements[$realization.useCase] + if (-not $supplierElement) { $supplierElement = $elementsByShortName[$realization.useCase] } + if (-not $supplierElement) { throw "use case not found for $($realization.id): $($realization.useCase)" } $realizationPackage = $analysisDesign.Packages.AddNew($realization.id, 'Package') $null = $realizationPackage.Update() $analysisDesign.Packages.Refresh() @@ -385,7 +390,7 @@ try { $realizationPackage.Elements.Refresh() $connector = $collaboration.Connectors.AddNew('', 'Realization') $connector.ClientID = $collaboration.ElementID - $connector.SupplierID = ($elements[$realization.useCase]).ElementID + $connector.SupplierID = $supplierElement.ElementID $null = $connector.Update() $collaboration.Connectors.Refresh()