sad-mcp 1.1.1 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sad-mcp",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "description": "MCP server for Software Analysis and Design course materials at BGU",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,13 +21,13 @@ The shared files prepended to this prompt define the layout recipes, model shape
21
21
 
22
22
  ## CRITICAL RULES — Check Before Every Output
23
23
 
24
- 1. **Hebrew: male form only.** אח (not אחות). מזכיר (not מזכירה). מטופל (not מטופלת). רופא (not רופאה). Verify every Hebrew label before writing.
24
+ 1. **Hebrew: male form only.** אח (not אחות), מזכיר (not מזכירה), מטופל (not מטופלת), רופא (not רופאה), לקוח (not לקוחה), מנהל (not מנהלת), סטודנט (not סטודנטית). Verify every Hebrew label before writing.
25
25
  2. **No generic catch-all use cases.** "ניהול הגדרות מערכת" is forbidden. Each entity → its own UC.
26
26
  3. **All ellipses have solid borders.** Dashed = relationship lines only, never ellipse borders.
27
- 4. **«include» target must have ≥2 incoming «include» arrows AND no direct actor association.** A UC connected to an actor can NEVER be an «include» target. A single «include» arrow is always wrong — fold it into the parent.
27
+ 4. **«include» requires BOTH: ≥2 base UCs point to it AND zero actor associations on the target.** Fail either half the «include» is wrong. A single incoming «include» is always wrong — fold into the parent. See §4 for the mandatory justification gate.
28
28
  5. **Every use case must have at least one association line to an actor.**
29
29
  6. **Actors must not overlap** — minimum 200px vertical distance between actor centers.
30
- 7. **Default: no «include» or «extend».** Most diagrams need zero or one. If you have more than two, you are almost certainly wrong. Treat every «include»/«extend» you consider adding as guilty until proven innocent.
30
+ 7. **Default: zero «include» and zero «extend».** Most diagrams need none. More than two is almost always wrong. §4 defines a gate every such relationship must pass before you draw it.
31
31
 
32
32
  ---
33
33
 
@@ -37,24 +37,24 @@ The shared files prepended to this prompt define the layout recipes, model shape
37
37
  |---|---|
38
38
  | **System name/boundary** | Names the rectangle |
39
39
  | **Human actors** (roles) | External humans who interact with the system |
40
- | **System actors** (external systems) | Draw as «system» rectangle |
40
+ | **System actors** (external systems) | Drawn as a stick figure with `«system»` stereotype (not a rectangle) |
41
41
  | **Actor generalizations** | Child actor does everything parent does + more |
42
42
  | **System administrator** | Almost always present — manages master data for every entity |
43
43
  | **Use cases** | One per user goal at goal-granularity level |
44
- | **Shared mandatory sub-behaviors** | «include» candidates — must be needed by 2+ UCs |
45
- | **Optional/conditional behaviors** | «extend» candidates |
44
+ | **Shared mandatory sub-behaviors** | «include» candidates — must pass the §4 gate |
45
+ | **Optional/conditional behaviors** | «extend» candidates — must pass the §4 gate |
46
46
  | **Language for labels** | Hebrew, English, or bilingual |
47
47
 
48
48
  ---
49
49
 
50
50
  ## 2. Actor Rules
51
51
 
52
- - **Human actor**: stick figure with name below
53
- - **System actor**: stick figure with `«system»` (italic, 9px) on the first label line and the actor name on the second line. Use a distinct head fill (`#fef3c7` amber) to visually distinguish from human actors. **Do NOT draw a rectangle** — UML rectangles for system actors are not taught in this course.
54
- - **Primary actors**: left side. **Secondary/admin actors**: right side. **External system actors** (e.g. LTI provider) that only connect to one UC: below the boundary
55
- - **Generalization**: solid line, hollow triangle at parent end. Route as **L-shape** (horizontal away from boundary, then vertical, then back) — never a straight diagonal that crosses other actors
52
+ - **Human actor**: stick figure with name below.
53
+ - **System actor**: stick figure with `«system»` (italic, 9px) on the first label line and the actor name on the second. Use a distinct head fill (`#fef3c7` amber) to visually distinguish from humans. **Do NOT draw a rectangle** — the course does not teach the rectangle form.
54
+ - **Positioning**: primary actors on the left, secondary/admin on the right, external systems that connect to only one UC below the boundary.
55
+ - **Generalization**: solid line, hollow triangle at the parent end. Route as an **L-shape** (horizontal away from boundary, then vertical, then back) — never a diagonal that crosses other actors.
56
56
 
57
- **System administrator** — add unless explicitly excluded. Responsible for managing the master data of every entity the system references. Each entity gets its own specific UC (not one catch-all).
57
+ **System administrator** — include unless explicitly excluded. Responsible for managing the master data of every entity the system references. Each entity gets its own specific UC (not one catch-all).
58
58
 
59
59
  ---
60
60
 
@@ -63,71 +63,82 @@ The shared files prepended to this prompt define the layout recipes, model shape
63
63
  **Granularity**: one complete user goal per UC, achievable in one session.
64
64
 
65
65
  **When to combine vs. separate "ניהול X":**
66
- - Simple entity (just a form): single "ניהול X" is fine
67
- - Complex entity with sub-items, lifecycle states, or different actors per operation: separate UCs (open / update / cancel / approve)
66
+ - Simple entity (just a form): single "ניהול X" is fine.
67
+ - Complex entity with sub-items, lifecycle states, or different actors per operation: separate UCs (open / update / cancel / approve).
68
68
 
69
69
  **Implied UCs**: every entity the system references needs at least one UC that manages it.
70
70
 
71
71
  ---
72
72
 
73
- ## 4. Include and Extend
73
+ ## 4. Include and Extend — Mandatory Justification Gate
74
74
 
75
- **Start with zero.** Add «include» or «extend» only when you can explicitly justify it against the tests below. When in doubt, leave it out.
75
+ **Default to zero.** Treat every «include» and «extend» as guilty until proven innocent. Before drawing ANY such relationship, write the relevant block below in your planning text. **If you cannot fill it in truthfully, the relationship does not exist — do not draw it.**
76
76
 
77
- ### «include»
78
- Allowed only when **all four** hold:
79
- 1. The sub-behavior is **mandatory** every time the base UC runs
80
- 2. The sub-behavior is shared by **2 or more** base UCs — there must be ≥2 incoming «include» arrows
81
- 3. The included UC has **no direct association to any actor** — if any actor connects to it, it is a regular UC, not an included sub-behavior
82
- 4. Extracting it genuinely simplifies the diagram — if the diagram becomes more complex, fold it back in
77
+ ### «include» justification block
83
78
 
84
- Arrow direction: Base UC → Included UC. Dashed line, open arrowhead.
79
+ ```
80
+ «include» candidate: __________________________________
81
+ Base UC #1 (actor-initiated, runs target every time): __________
82
+ Base UC #2 (actor-initiated, runs target every time): __________
83
+ Target has zero direct actor associations? yes / no
84
+ Extracting reduces real duplication (not just "feels like a step")? yes / no
85
+
86
+ → If Base UC #2 is blank → DELETE the «include». Fold into Base UC #1.
87
+ → If actor-associations = yes → DELETE. The target is a regular UC, not a helper.
88
+ → If duplication reduction = no → DELETE. A step inside one UC is just a step.
89
+ ```
90
+
91
+ Semantics: «include» means the target runs **every single time** the base runs, unconditionally. It is never "sometimes X". That is «extend».
92
+
93
+ Arrow: dashed, open arrowhead, `Base UC → Included UC`.
85
94
 
86
- **Immediate disqualifiers if any apply, do NOT use «include»:**
87
- - Only one base UC points to it → fold into the parent
88
- - An actor has an association line to it → it is a standalone UC
89
- - You added it just to avoid repeating a label → wrong reason; keep behavior inside the UC
95
+ ### «extend» justification block
90
96
 
91
- ### «extend»
92
- Allowed only when:
93
- 1. The behavior is **genuinely optional or conditional** (not just infrequent)
94
- 2. The extension would clutter the base UC if included inside it
97
+ ```
98
+ «extend» candidate: __________________________________
99
+ Base UC being extended: __________
100
+ Exact trigger: "when __________ happens" (one sentence, specific condition)
101
+ Base UC runs correctly without the extension? yes / no
102
+ Is this behavior significant enough to model (not every trivial optional path)? yes / no
103
+
104
+ → If trigger is vague or cannot be stated in one sentence → DELETE.
105
+ → If base UC does NOT run correctly without it → it is not «extend». Reconsider as «include» (if always) or as part of the base UC.
106
+ → If insignificant → DELETE. Not every optional step deserves «extend».
107
+ ```
95
108
 
96
- Arrow direction: Extension UC → Base UC. Use at most once or twice per diagram.
109
+ Arrow: dashed, open arrowhead, `Extension UC → Base UC`. Use at most once or twice per diagram.
97
110
 
98
- **If you cannot articulate the exact condition under which the extension fires, do not add it.**
111
+ ### Quick disqualifiers
112
+
113
+ - Exactly one incoming «include» arrow → fold into the parent.
114
+ - Actor directly connects to an alleged «include» target → it is a standalone UC, not a sub-behavior.
115
+ - "Always happens as part of X" alone is not enough — it must also be shared by ≥2 bases.
116
+ - Conditional or optional behavior → this is «extend» territory, not «include».
99
117
 
100
118
  ---
101
119
 
102
120
  ## 5. Notation
103
121
 
104
- ### Actors
105
- ```
106
- Human: stick figure — circle (head) + lines (body/arms/legs), name below
107
- System: rectangle with «system» stereotype label, name inside
108
- Generalization: solid line, hollow triangle at parent end, L-shaped routing
109
- ```
122
+ Colors, stroke widths, and font sizes live in §10 Styling. Line-type semantics and canonical arrow directions are owned by `NOTATION.md §3`. This section captures only the implementation-specific details the generator needs.
123
+
124
+ ### Actor rendering
125
+ - Human: stick figure head circle + body/arms/legs lines, name below.
126
+ - System: same stick figure with amber head fill and `«system»` label line above the name.
127
+ - Generalization: solid line, hollow triangle at parent end, L-shaped routing.
110
128
 
111
129
  ### System boundary
112
130
  Large rounded rectangle (`rx=12`), title bar on top with system name (white text on `#1e40af` background).
113
131
 
114
132
  ### Use case ellipses
115
- - Horizontal ellipse: `rx` 70–125, `ry` 25–32 (size to fit text)
116
- - Fill: `#d4e9f7`, solid border `#2980b9` (1.8px) — **always solid, no exceptions**
117
- - Text: centered, 11px, may wrap 2 lines
118
-
119
- ### Association lines
120
- - Actor ↔ UC: solid line, **no arrowhead**
121
- - «include»/«extend»: dashed (dasharray 6 4), open arrowhead, `#7c3aed` stroke
122
- - Stereotype label (6 4 dashed line): 9.5px near midpoint
133
+ - Horizontal ellipse: `rx` 70–125, `ry` 25–32 (size to fit text).
134
+ - Fill `#d4e9f7`, solid border `#2980b9` (1.8px) — **always solid, no exceptions**.
135
+ - Text centered, 11px, may wrap to 2 lines.
123
136
 
124
137
  ### Ellipse endpoint calculation (mandatory)
125
-
126
- Use `ellipse-boundary-intersect` from the shared layout recipes for all line endpoints. Never draw a line to the ellipse center — it disappears under the fill.
138
+ Never draw a line to the ellipse center — it disappears under the fill. Compute the boundary intersection:
127
139
 
128
140
  ```javascript
129
- // From shared layout-recipes.md:
130
- // ellipse-boundary-intersect(cx, cy, rx, ry, externalPoint)
141
+ // Shared layout-recipes.md: ellipse-boundary-intersect(cx, cy, rx, ry, externalPoint)
131
142
  angle = Math.atan2(externalPoint.y - cy, externalPoint.x - cx);
132
143
  endpoint = { x: cx + rx * Math.cos(angle), y: cy + ry * Math.sin(angle) };
133
144
  ```
@@ -136,149 +147,20 @@ endpoint = { x: cx + rx * Math.cos(angle), y: cy + ry * Math.sin(angle) };
136
147
  1. Background + dot grid
137
148
  2. System boundary rectangle
138
149
  3. Association lines (solid)
139
- 4. «include»/«extend» dashed lines with labels
150
+ 4. «include» / «extend» dashed lines with labels
140
151
  5. Generalization lines with triangles
141
152
  6. Use case ellipses (covers line endpoints — draw last among shapes)
142
153
  7. Actor figures and labels
143
154
 
144
- No legend. Do not add a legend bar to the diagram.
145
-
146
- ---
147
-
148
- ## 6. Layout Algorithm
149
-
150
- **The Fix Layout algorithm (§8) is the primary placement path.** Run it on `DOMContentLoaded` — do not rely on initial coordinates being perfect. Initial coordinates are starting hints only.
151
-
152
- ### Initial placement
153
-
154
- **Step 1 — Assign UCs to actor groups**
155
- For each UC, identify its primary actor (the one with the most associations or the initiating actor). Assign the UC to that actor's side (left/right/center).
156
-
157
- **Step 2 — Sort actors with `actor-adjacency-sort` (from shared layout-recipes.md)**
158
- Before placing actors, sort actors on each side by the barycentric Y of their UC groups to minimize line crossings.
159
-
160
- **Step 3 — Initial UC positions with `row-grid-pack`**
161
- Place UCs in each column using `row-grid-pack`:
162
- ```
163
- left column: x = boundary.x + boundary.width * 0.27
164
- right column: x = boundary.x + boundary.width * 0.73
165
- center column: x = boundary.x + boundary.width * 0.50
166
- rowHeight = max(ry * 2 + 80, 80)
167
- ```
168
-
169
- **Step 4 — Place actors at vertical center of their UC groups**
170
- ```javascript
171
- actor.y = average(actor.ucIds.map(id => useCases[id].cy));
172
- actor.x = side === 'left' ? boundary.x - 120 : boundary.x + boundary.width + 120;
173
- ```
174
-
175
- **Step 5 — Run `collision-nudge` on UCs** (from shared layout-recipes.md)
176
-
177
- **Step 6 — Run `validateLayout()` then `fixLayout()` on `DOMContentLoaded`**
178
-
179
- `validateLayout` (§8a) must run before applying positions. It catches three problems the placement step does not: UCs overlapping the header bar, UCs overlapping each other within a column, and UCs overflowing the boundary bottom. After validation, boundary height and SVG viewBox are expanded automatically.
180
-
181
- ```javascript
182
- document.addEventListener('DOMContentLoaded', () => {
183
- validateLayout(part); // fix positions, expand boundary if needed
184
- applyLayout(svgId, part); // write to SVG
185
- });
186
- ```
155
+ **No legend.** Do not add a legend bar to the diagram.
187
156
 
188
157
  ---
189
158
 
190
- ## 7. Splitting Rules
159
+ ## 6. Layout
191
160
 
192
- Split into multiple tabs when **use case count > 8** OR when the system clearly has distinct functional areas (clinical vs. admin, customer vs. back-office).
161
+ One entry point: `fixLayout(layout)`. It runs on `DOMContentLoaded` **and** whenever the Fix Layout button is clicked. It is idempotent clicking 1 or 10 times produces the same result. Initial `cx`/`cy` coordinates you write in the SVG are starting hints; `fixLayout` is the authority.
193
162
 
194
- **How to split:**
195
- - One tab per **actor group** (e.g., "מטופל" tab, "מנהל" tab) or functional area
196
- - Shared UCs appear in both tabs with a "(shared with Tab N)" badge
197
- - Each tab has its own complete SVG (its own boundary, actors, ellipses, lines)
198
- - Each tab has a visible **split rationale line**: `"Split by actor group: [actor names] — [N] UCs > 8 threshold"`
199
- - Run `fixLayout()` independently per tab on load
200
-
201
- For tabbed layouts: `diagramLayouts` array, one entry per tab.
202
-
203
- ---
204
-
205
- ## 8a. validateLayout — Boundary & Overlap Validation (Required)
206
-
207
- Run this **before** writing positions to the SVG. It is idempotent.
208
-
209
- ```javascript
210
- // Named constants — tune these, do not hardcode magic numbers elsewhere
211
- const HEADER_H = 44; // boundary header height in px
212
- const UC_PAD = 30; // min gap from boundary edge to nearest UC edge
213
- const UC_GAP = 14; // min gap between UC ellipses in the same column
214
-
215
- function validateLayout(part) {
216
- const b = part.boundary;
217
- const ucs = part.useCases;
218
-
219
- const minX = b.x + UC_PAD;
220
- const maxX = b.x + b.w - UC_PAD;
221
- const minY = b.y + HEADER_H + UC_PAD;
222
-
223
- let changed = true, iter = 0;
224
- while (changed && iter++ < 40) {
225
- changed = false;
226
-
227
- // 1. Clamp horizontal positions inside boundary
228
- for (const uc of ucs) {
229
- const nx = Math.max(minX + uc.rx, Math.min(maxX - uc.rx, uc.cx));
230
- if (nx !== uc.cx) { uc.cx = nx; changed = true; }
231
- }
232
-
233
- // 2. Push UCs below header
234
- for (const uc of ucs) {
235
- const ny = Math.max(minY + uc.ry, uc.cy);
236
- if (ny !== uc.cy) { uc.cy = ny; changed = true; }
237
- }
238
-
239
- // 3. Resolve vertical overlaps within each column
240
- const cols = {};
241
- for (const uc of ucs) {
242
- const col = uc.col || 'center';
243
- (cols[col] = cols[col] || []).push(uc);
244
- }
245
- for (const col of Object.values(cols)) {
246
- col.sort((a, b) => a.cy - b.cy);
247
- for (let i = 1; i < col.length; i++) {
248
- const prev = col[i-1], cur = col[i];
249
- const needed = prev.cy + prev.ry + UC_GAP + cur.ry;
250
- if (cur.cy < needed) { cur.cy = needed; changed = true; }
251
- }
252
- }
253
- }
254
-
255
- // 4. Expand boundary height to fit all UCs
256
- const maxBottom = Math.max(...ucs.map(uc => uc.cy + uc.ry));
257
- const requiredH = maxBottom - b.y + UC_PAD;
258
- if (requiredH > b.h) b.h = requiredH;
259
-
260
- // 5. Re-center left/right actors on their UC groups
261
- for (const actor of part.actors) {
262
- if (actor.side === 'left' || actor.side === 'right') {
263
- const myUCIds = part.connections.filter(c => c.from === actor.id).map(c => c.to);
264
- const myUCs = ucs.filter(uc => myUCIds.includes(uc.id));
265
- if (myUCs.length) actor.y = myUCs.reduce((s, uc) => s + uc.cy, 0) / myUCs.length;
266
- }
267
- // Push bottom actors below expanded boundary
268
- if (actor.side === 'bottom') actor.y = b.y + b.h + 80;
269
- }
270
- }
271
- ```
272
-
273
- **When to call**: always before `applyLayout`. Both on `DOMContentLoaded` and when the Fix Layout button is clicked.
274
-
275
- ---
276
-
277
- ## 8. Fix Layout Algorithm (Primary Placement Path)
278
-
279
- This algorithm runs automatically on page load AND is triggered by the "Fix Layout" button. It is **idempotent** — clicking 1 or 10 times produces the same result.
280
-
281
- ### Required data structure
163
+ ### Data structure
282
164
 
283
165
  ```javascript
284
166
  const diagramLayout = {
@@ -301,21 +183,27 @@ const diagramLayout = {
301
183
  };
302
184
  ```
303
185
 
304
- ### SVG element conventions (required for fix algorithm)
186
+ ### SVG element conventions (required)
305
187
 
306
- - **UC groups**: `<g id="uc-group-{ucId}">` with `<ellipse cx="..." cy="...">` (NOT transforms for UC positioning)
307
- - **Actor groups**: `<g id="actor-group-{actorId}" transform="translate(x,y)">`
308
- - **Connection lines**: `<line data-conn-from="{id}" data-conn-to="{id}" x1 y1 x2 y2>`
188
+ - UC groups: `<g id="uc-group-{ucId}">` with `<ellipse cx="..." cy="...">` — do NOT use `transform` to position UCs.
189
+ - Actor groups: `<g id="actor-group-{actorId}" transform="translate(x,y)">`.
190
+ - Connection lines: `<line data-conn-from="{id}" data-conn-to="{id}" x1 y1 x2 y2>`.
309
191
 
310
- ### Fix algorithm
192
+ ### fixLayout implementation
311
193
 
312
194
  ```javascript
195
+ // Named constants — tune these, do not hardcode magic numbers elsewhere.
196
+ const HEADER_H = 44; // boundary header height
197
+ const UC_PAD = 30; // min gap from boundary edge to nearest UC edge
198
+ const UC_GAP = 14; // min gap between UC ellipses in the same column
199
+ const ACTOR_PAD = 80; // horizontal gap between boundary and actor column
200
+
313
201
  function fixLayout(layout) {
314
202
  const svg = document.getElementById(layout.svgId);
315
203
  if (!svg) return;
316
- const pad = 80;
204
+ const b = layout.boundary;
317
205
 
318
- // Step 1: Group UCs by actor side
206
+ // Step 1 Group UCs by the side of their owning actor.
319
207
  const ucSide = {};
320
208
  layout.actors.forEach(a =>
321
209
  a.ucIds.forEach(uid => {
@@ -325,41 +213,69 @@ function fixLayout(layout) {
325
213
  const cols = { left: [], right: [], center: [] };
326
214
  layout.useCases.forEach(uc => cols[ucSide[uc.id] || 'center'].push(uc));
327
215
 
328
- // Step 2: Sort each column by actor order (top-to-bottom on that side)
216
+ // Step 2 Sort each column by actor order (top-to-bottom on that side).
329
217
  function sortCol(ucs, side) {
330
- const ordered = layout.actors.filter(a => a.side === side).sort((a,b) => a.y - b.y);
331
- ucs.sort((a, b) => {
218
+ const ordered = layout.actors.filter(a => a.side === side).sort((a, bb) => a.y - bb.y);
219
+ ucs.sort((a, bb) => {
332
220
  const ai = ordered.findIndex(act => act.ucIds.includes(a.id));
333
- const bi = ordered.findIndex(act => act.ucIds.includes(b.id));
221
+ const bi = ordered.findIndex(act => act.ucIds.includes(bb.id));
334
222
  return (ai < 0 ? 999 : ai) - (bi < 0 ? 999 : bi);
335
223
  });
336
224
  }
337
- sortCol(cols.left, 'left');
225
+ sortCol(cols.left, 'left');
338
226
  sortCol(cols.right, 'right');
339
227
 
340
- // Step 3: Assign Y positions
341
- const bx = layout.boundary.x, bw = layout.boundary.width, by = layout.boundary.y;
228
+ // Step 3 Initial Y assignment per column.
342
229
  function place(ucs, colX) {
343
230
  ucs.forEach((uc, i) => {
344
- uc.cy = by + 70 + i * (uc.ry * 2 + pad) + uc.ry;
231
+ uc.cy = b.y + HEADER_H + UC_PAD + uc.ry + i * (uc.ry * 2 + UC_GAP);
345
232
  uc.cx = colX;
233
+ uc.col = ucSide[uc.id] || 'center';
346
234
  });
347
235
  }
348
- place(cols.left, bx + bw * 0.28);
349
- place(cols.right, bx + bw * 0.72);
350
- place(cols.center, bx + bw * 0.50);
236
+ place(cols.left, b.x + b.width * 0.28);
237
+ place(cols.right, b.x + b.width * 0.72);
238
+ place(cols.center, b.x + b.width * 0.50);
239
+
240
+ // Step 4 — Clamp and resolve overlaps. Idempotent fixed-point loop.
241
+ const minX = b.x + UC_PAD;
242
+ const maxX = b.x + b.width - UC_PAD;
243
+ const minY = b.y + HEADER_H + UC_PAD;
244
+ let changed = true, iter = 0;
245
+ while (changed && iter++ < 40) {
246
+ changed = false;
247
+ for (const uc of layout.useCases) {
248
+ const nx = Math.max(minX + uc.rx, Math.min(maxX - uc.rx, uc.cx));
249
+ if (nx !== uc.cx) { uc.cx = nx; changed = true; }
250
+ const ny = Math.max(minY + uc.ry, uc.cy);
251
+ if (ny !== uc.cy) { uc.cy = ny; changed = true; }
252
+ }
253
+ for (const col of Object.values(cols)) {
254
+ col.sort((a, bb) => a.cy - bb.cy);
255
+ for (let i = 1; i < col.length; i++) {
256
+ const prev = col[i - 1], cur = col[i];
257
+ const needed = prev.cy + prev.ry + UC_GAP + cur.ry;
258
+ if (cur.cy < needed) { cur.cy = needed; changed = true; }
259
+ }
260
+ }
261
+ }
351
262
 
352
- // Step 4: Recenter actors on their UC groups
263
+ // Step 5 Expand boundary height to fit all UCs.
264
+ const maxBottom = Math.max(...layout.useCases.map(uc => uc.cy + uc.ry));
265
+ b.height = Math.max(b.height, maxBottom - b.y + UC_PAD);
266
+
267
+ // Step 6 — Re-center actors on their UC group; push bottom actors below boundary.
353
268
  layout.actors.forEach(actor => {
354
269
  const myUCs = layout.useCases.filter(u => actor.ucIds.includes(u.id));
355
- if (myUCs.length) actor.y = myUCs.reduce((s,u) => s + u.cy, 0) / myUCs.length;
270
+ if (actor.side === 'left' || actor.side === 'right') {
271
+ if (myUCs.length) actor.y = myUCs.reduce((s, u) => s + u.cy, 0) / myUCs.length;
272
+ actor.x = actor.side === 'left' ? b.x - ACTOR_PAD : b.x + b.width + ACTOR_PAD;
273
+ } else if (actor.side === 'bottom') {
274
+ actor.y = b.y + b.height + ACTOR_PAD;
275
+ }
356
276
  });
357
277
 
358
- // Step 5: Expand boundary height if needed
359
- const maxY = Math.max(...layout.useCases.map(u => u.cy + u.ry));
360
- layout.boundary.height = Math.max(layout.boundary.height, maxY - by + 60);
361
-
362
- // Step 6: Apply positions — absolute attributes only, never cumulative
278
+ // Step 7 Write UC positions to the SVG (absolute attributes; translate deltas on text).
363
279
  layout.useCases.forEach(uc => {
364
280
  const g = document.getElementById('uc-group-' + uc.id);
365
281
  if (!g) return;
@@ -380,7 +296,7 @@ function fixLayout(layout) {
380
296
  if (g) g.setAttribute('transform', `translate(${actor.x},${actor.y})`);
381
297
  });
382
298
 
383
- // Step 7: Recalculate all line endpoints using ellipse-boundary-intersect
299
+ // Step 8 Recalculate every connection line endpoint.
384
300
  svg.querySelectorAll('[data-conn-from][data-conn-to]').forEach(line => {
385
301
  const fId = line.getAttribute('data-conn-from');
386
302
  const tId = line.getAttribute('data-conn-to');
@@ -388,23 +304,23 @@ function fixLayout(layout) {
388
304
  const fAc = layout.actors.find(a => a.id === fId);
389
305
  const tUC = layout.useCases.find(u => u.id === tId);
390
306
  const tAc = layout.actors.find(a => a.id === tId);
391
- const from = fAc ? {x: fAc.x, y: fAc.y} : fUC ? {x: fUC.cx, y: fUC.cy} : null;
392
- const to = tAc ? {x: tAc.x, y: tAc.y} : tUC ? {x: tUC.cx, y: tUC.cy} : null;
307
+ const from = fAc ? { x: fAc.x, y: fAc.y } : fUC ? { x: fUC.cx, y: fUC.cy } : null;
308
+ const to = tAc ? { x: tAc.x, y: tAc.y } : tUC ? { x: tUC.cx, y: tUC.cy } : null;
393
309
  if (!from || !to) return;
394
310
  let x1 = from.x, y1 = from.y, x2 = to.x, y2 = to.y;
395
- if (fUC) { const a = Math.atan2(to.y-fUC.cy, to.x-fUC.cx); x1=fUC.cx+fUC.rx*Math.cos(a); y1=fUC.cy+fUC.ry*Math.sin(a); }
396
- if (tUC) { const a = Math.atan2(from.y-tUC.cy, from.x-tUC.cx); x2=tUC.cx+tUC.rx*Math.cos(a); y2=tUC.cy+tUC.ry*Math.sin(a); }
397
- line.setAttribute('x1',x1); line.setAttribute('y1',y1);
398
- line.setAttribute('x2',x2); line.setAttribute('y2',y2);
311
+ if (fUC) { const a = Math.atan2(to.y - fUC.cy, to.x - fUC.cx); x1 = fUC.cx + fUC.rx * Math.cos(a); y1 = fUC.cy + fUC.ry * Math.sin(a); }
312
+ if (tUC) { const a = Math.atan2(from.y - tUC.cy, from.x - tUC.cx); x2 = tUC.cx + tUC.rx * Math.cos(a); y2 = tUC.cy + tUC.ry * Math.sin(a); }
313
+ line.setAttribute('x1', x1); line.setAttribute('y1', y1);
314
+ line.setAttribute('x2', x2); line.setAttribute('y2', y2);
399
315
  });
400
316
 
401
- // Step 8: Resize boundary rect + viewBox
317
+ // Step 9 Resize boundary rect and SVG viewBox.
402
318
  const br = svg.querySelector('.system-boundary, rect[rx="12"]');
403
- if (br) br.setAttribute('height', layout.boundary.height);
319
+ if (br) br.setAttribute('height', b.height);
404
320
  const vb = svg.getAttribute('viewBox');
405
321
  if (vb) {
406
322
  const p = vb.split(/\s+/);
407
- p[3] = Math.max(parseFloat(p[3]), by + layout.boundary.height + 80);
323
+ p[3] = Math.max(parseFloat(p[3]), b.y + b.height + 120);
408
324
  svg.setAttribute('viewBox', p.join(' '));
409
325
  }
410
326
  }
@@ -427,9 +343,39 @@ function fixLayout(layout) {
427
343
 
428
344
  ---
429
345
 
430
- ## 9. Clickable Descriptions for UCs and Actors (Always Include)
346
+ ## 7. Splitting Into Multiple Diagrams
347
+
348
+ Split the use case diagram into multiple tabs in one HTML file when **either** trigger fires:
349
+
350
+ 1. **Total UC count > 8.**
351
+ 2. **≥2 actor groups each own ≥3 UCs, and those groups share no UCs with each other** (truly disjoint responsibilities — e.g., clinical side vs. admin side).
352
+
353
+ If neither trigger fires, produce a single tab. Do not split on gut feeling.
354
+
355
+ ### Examples
356
+
357
+ | Scenario | UC count | Actor groups | Split? | Reason |
358
+ |---|---|---|---|---|
359
+ | Small library system | 5 | 1 (reader) | **No** | Both triggers fail |
360
+ | Coffee shop | 7 | 2 (customer 4 UCs, barista 3 UCs) disjoint | **Yes** | Trigger #2 fires |
361
+ | Hospital clinic | 12 | 1 (doctor) owns most | **Yes** | Trigger #1 fires |
362
+ | Course registration | 8 | 2 (student 5, admin 3) — share "login" only | **Yes** | Trigger #2 fires (login handled via cross-tab stub) |
363
+
364
+ ### Mechanics
365
+
366
+ - One tab per actor group or per functional area.
367
+ - **Cross-tab connections:** draw the stub on each side with the partner's name and a `(→ Tab N)` label. Do NOT try to route a line across tabs.
368
+ - Each tab has its own complete SVG (its own boundary, actors, ellipses, lines).
369
+ - Each tab has a visible **split-rationale line** below the tab bar. Use whichever trigger actually fired:
370
+ - `"Split by UC count: 12 UCs > 8 threshold"` or
371
+ - `"Split by actor groups: [group A] / [group B] — disjoint responsibilities"`.
372
+ - Store all tabs in a `diagramLayouts` array (one entry per tab). Call `fixLayout(diagramLayouts[i])` for every tab on `DOMContentLoaded`. See §11 for the `showDiagram(index)` wiring.
373
+
374
+ ---
375
+
376
+ ## 8. Clickable Descriptions for UCs and Actors (Always Include)
431
377
 
432
- Every UC ellipse **and every actor** must be clickable — click shows a floating panel with name + description. Use a single `showDesc(id, type, event)` function for both.
378
+ Every UC ellipse **and** every actor must be clickable — click shows a floating panel with name + description. One `showDesc(id, type, event)` function handles both.
433
379
 
434
380
  ```javascript
435
381
  const useCaseDescriptions = {
@@ -472,27 +418,26 @@ document.addEventListener('click', e => {
472
418
  ```
473
419
 
474
420
  UC group: `<g id="uc-uc1" data-uc-id="uc1" onclick="showDesc('uc1','uc',event)" style="cursor:pointer">`
475
-
476
421
  Actor group: `<g id="actor-student" data-actor-id="student" onclick="showDesc('student','actor',event)" style="cursor:pointer">`
477
422
 
478
- **Association label (timing/frequency):** When an actor connection has a timing annotation (e.g., "פעם ביום" for a system cron actor), add a `<text>` element with a white `<rect>` behind it at the midpoint of the line. Position the midpoint after `applyLayout` computes the endpoints.
423
+ **Association labels (timing/frequency):** when an actor connection has a timing annotation (e.g., "פעם ביום" for a cron actor), add a `<text>` with a white `<rect>` behind it at the midpoint of the line after `fixLayout` has set the endpoints.
479
424
 
480
425
  ---
481
426
 
482
- ## 10. diagramModel (mandatory)
427
+ ## 9. diagramModel (mandatory)
483
428
 
484
- Define this variable in every generated HTML `<script>` block, following the `model-shape.md` spec for `kind: 'use-case'`. Every actor, use case, and relationship must be represented. The VP exporter walks this variable.
429
+ Define this variable in every generated HTML `<script>` block, following the `model-shape.md` spec for `kind: 'use-case'`. Every actor, use case, and relationship must be represented. The Visual Paradigm exporter walks this variable.
485
430
 
486
431
  ---
487
432
 
488
- ## 11. Styling
433
+ ## 10. Styling
489
434
 
490
435
  ```
491
436
  Background: #f8fafc, dot-grid (24px, #cbd5e1, r=0.8)
492
437
  System boundary: fill white, stroke #3b82f6 (2.5px), header fill #1e40af, rx=12
493
438
  Ellipse: fill #d4e9f7, stroke #2980b9 (1.8px), solid
494
439
  Ellipse text: #1a1a2e, 11px, weight 500, text-anchor middle
495
- Actor stick figure: stroke #334155 (2px), head fill #e0f0ff
440
+ Actor stick figure: stroke #334155 (2px), human head fill #e0f0ff, system head fill #fef3c7
496
441
  Actor label: #1e293b, 11px, weight 600, text-anchor middle
497
442
  Association line: #334155, 1.8px, no arrowhead
498
443
  Include/extend: #7c3aed, 1.6px, dasharray 6 4, open arrowhead, label 9.5px
@@ -503,7 +448,7 @@ Fonts: 'Noto Sans Hebrew' + 'IBM Plex Mono' from Google Fonts
503
448
 
504
449
  ---
505
450
 
506
- ## 12. HTML File Structure
451
+ ## 11. HTML File Structure
507
452
 
508
453
  ```html
509
454
  <!DOCTYPE html>
@@ -516,86 +461,84 @@ Fonts: 'Noto Sans Hebrew' + 'IBM Plex Mono' from Google Fonts
516
461
  </head>
517
462
  <body>
518
463
  <!-- Tab bar (only when split) -->
519
- <div class="tab-bar">...</div>
464
+ <div class="tab-bar">
465
+ <button class="tab active" onclick="showDiagram(0)">[Tab 1 label]</button>
466
+ <button class="tab" onclick="showDiagram(1)">[Tab 2 label]</button>
467
+ </div>
468
+
469
+ <!-- Split rationale (only when split) -->
470
+ <div class="split-rationale">Split by UC count: N UCs &gt; 8 threshold</div>
520
471
 
521
472
  <!-- Fix Layout button (above each diagram) -->
522
473
  <div style="text-align:center;margin:8px 0;">
523
- <button onclick="fixLayout(diagramLayout)" class="fix-btn">Fix Layout</button>
474
+ <button onclick="fixLayout(currentLayout())" class="fix-btn">Fix Layout</button>
524
475
  </div>
525
476
 
526
- <!-- Split rationale (only when split) -->
527
- <div class="split-rationale">Split by actor group: [...] — N UCs > 8 threshold</div>
528
-
529
- <!-- Diagram SVG -->
530
- <div id="diagram-0">
531
- <svg id="diagram-svg" viewBox="0 0 1100 900" xmlns="http://www.w3.org/2000/svg">
477
+ <!-- One container per tab; inactive tabs are hidden. -->
478
+ <div class="diagram-part" id="diagram-0">
479
+ <svg id="diagram-svg-0" viewBox="0 0 1100 900" xmlns="http://www.w3.org/2000/svg">
532
480
  <!-- dot grid, boundary, association lines, include/extend lines,
533
481
  generalization lines, UC ellipse groups, actor groups -->
534
482
  </svg>
535
483
  </div>
484
+ <div class="diagram-part" id="diagram-1" hidden>
485
+ <svg id="diagram-svg-1" viewBox="0 0 1100 900" xmlns="http://www.w3.org/2000/svg">...</svg>
486
+ </div>
536
487
 
537
488
  <!-- VP export button -->
538
489
  <button class="vp-export-btn" onclick="exportXMI()">Download .xmi (Visual Paradigm)</button>
539
490
 
540
491
  <script>
541
- const diagramLayout = { ... }; // §8 data structure
542
- const useCaseDescriptions = { ... }; // §9
543
- const diagramModel = { ... }; // model-shape.md for kind:'use-case'
544
- function fixLayout(layout) { ... } // §8 algorithm
545
- function showDescription(id, e) { ... } // §9
546
- function hideDescription() { ... }
547
- function xmiBody(elements, rels) { ... } // export-buttons.md
548
- function exportXMI() { ... }
549
- function xmlEscape(s) { ... }
550
- function downloadFile(...) { ... }
551
- // Run layout on page load:
552
- document.addEventListener('DOMContentLoaded', () => fixLayout(diagramLayout));
492
+ const diagramLayouts = [ /* one layout object per tab, per §6 */ ];
493
+ const useCaseDescriptions = { /* §8 */ };
494
+ const actorDescriptions = { /* §8 */ };
495
+ const diagramModel = { /* model-shape.md, kind: 'use-case' */ };
496
+
497
+ let activeTab = 0;
498
+ function currentLayout() { return diagramLayouts[activeTab]; }
499
+ function showDiagram(index) {
500
+ activeTab = index;
501
+ document.querySelectorAll('.diagram-part').forEach((p, i) => p.hidden = i !== index);
502
+ document.querySelectorAll('.tab').forEach((t, i) => t.classList.toggle('active', i === index));
503
+ fixLayout(diagramLayouts[index]); // idempotent
504
+ }
505
+
506
+ function fixLayout(layout) { /* §6 */ }
507
+ function showDesc(id, type, event) { /* §8 */ }
508
+ function hideDesc() { /* §8 */ }
509
+ function xmiBody(elements, rels) { /* export-buttons.md */ }
510
+ function exportXMI() { /* export-buttons.md */ }
511
+
512
+ document.addEventListener('DOMContentLoaded', () => {
513
+ diagramLayouts.forEach(l => fixLayout(l));
514
+ });
553
515
  </script>
554
516
  </body>
555
517
  </html>
556
518
  ```
557
519
 
558
- For **tabbed layouts**, add the `showDiagram(index)` function and run `fixLayout(diagramLayouts[i])` for each tab index on `DOMContentLoaded`.
520
+ For a single-tab diagram, drop the tab bar, the split-rationale line, and the second `.diagram-part`; keep `diagramLayouts` as a one-element array so `showDiagram` / `fixLayout` wiring stays uniform.
559
521
 
560
522
  ---
561
523
 
562
- ## 13. Output
563
-
564
- - Save as `[system_name]_use_case_diagram.html`
565
- - Briefly state: actor count, use case count, include/extend relationships, whether split (and why)
566
-
567
- ---
524
+ ## 12. Output
568
525
 
569
- ## 14. Pre-Delivery Checklist
570
-
571
- - [ ] Every Hebrew label uses male form — אח not אחות, מזכיר not מזכירה, מטופל not מטופלת
572
- - [ ] No generic "ניהול הגדרות מערכת" or combined-entity UCs
573
- - [ ] All ellipses have solid borders (zero dashed ellipses)
574
- - [ ] Every «include» has ≥2 incoming arrows; included UC has no direct actor association
575
- - [ ] Every UC has ≥1 association line to an actor (no orphaned ellipses)
576
- - [ ] Actors ≥200px vertical distance from each other
577
- - [ ] SVG draw order: boundary → lines → ellipses → actors
578
- - [ ] `validateLayout()` runs before `applyLayout()` — both on load and on Fix Layout click
579
- - [ ] No UC overlaps the header bar (uc.cy - uc.ry >= boundary.y + HEADER_H + UC_PAD)
580
- - [ ] No two UCs in the same column overlap each other (gap >= UC_GAP)
581
- - [ ] Boundary height auto-expanded if UCs exceed initial height
582
- - [ ] SVG viewBox large enough to show all actors including bottom actors
583
- - [ ] Ellipse endpoints computed via `ellipsePt` (not drawn to center)
584
- - [ ] UC description popups wired to every `data-uc-id` element
585
- - [ ] Actor description popups wired to every `data-actor-id` element
586
- - [ ] System actors drawn as stick figures with `«system»` italic label (not rectangles)
587
- - [ ] Bottom actors (external systems connecting only below) positioned below boundary
588
- - [ ] If split: each tab has a split-rationale line; layout applied independently per tab
589
- - [ ] `diagramModel` defined per model-shape.md
590
- - [ ] VP export button present and wired to `exportXMI()`
526
+ - Save as `[system_name]_use_case_diagram.html`.
527
+ - Briefly state: actor count, use case count, include/extend relationships (with the justification line for each), whether split (and which trigger fired).
591
528
 
592
529
  ---
593
530
 
594
- ## 15. Hebrew Language Guidelines
595
-
596
- Always use **male grammatical form** (זכר) for all Hebrew actor and role names.
597
-
598
- רופא, אח, מטופל, לקוח, מנהל, עובד, משתמש, סטודנט, מרצה
599
- אחות, מטופלת, לקוחה, מנהלת, מזכירה, סטודנטית
600
-
601
- When in doubt, use the male form.
531
+ ## 13. Pre-Delivery Checklist
532
+
533
+ - [ ] Every «include» and «extend» has a written §4 justification block; any that cannot be filled in has been deleted.
534
+ - [ ] No «include» with exactly one incoming arrow — every include target has ≥2 bases AND zero actor associations.
535
+ - [ ] No generic "ניהול הגדרות מערכת" or combined-entity UCs; each entity has its own UC.
536
+ - [ ] Every UC has ≥1 association line to an actor (no orphaned ellipses).
537
+ - [ ] All ellipses have solid borders (zero dashed ellipses).
538
+ - [ ] SVG draw order: boundary → lines → ellipses → actors.
539
+ - [ ] Ellipse endpoints computed via boundary intersection (not drawn to center).
540
+ - [ ] `fixLayout` runs on `DOMContentLoaded` and on the Fix Layout button; uses named constants (HEADER_H, UC_PAD, UC_GAP, ACTOR_PAD), not magic numbers.
541
+ - [ ] UC and actor description popups wired to every `data-uc-id` / `data-actor-id` element.
542
+ - [ ] System actors drawn as stick figures with `«system»` label (not rectangles).
543
+ - [ ] If split: each tab has a split-rationale line citing the actual trigger that fired (count > 8, or 2+ disjoint actor groups); `showDiagram` and per-tab `fixLayout` wired per §11.
544
+ - [ ] `diagramModel` defined per `model-shape.md`; VP export button present and wired to `exportXMI()`.