sad-mcp 1.1.2 → 1.1.4

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.2",
3
+ "version": "1.1.4",
4
4
  "description": "MCP server for Software Analysis and Design course materials at BGU",
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,13 +21,15 @@ 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
+ 8. **UC labels describe actions WITH the information system, not physical real-world actions.** See §3 for the full rule and verb list.
32
+ 9. **Every system-time / cron actor association MUST carry a timing label** (e.g., "פעם ביום", "כל 5 דקות"). See §2 and §8 for rendering.
31
33
 
32
34
  ---
33
35
 
@@ -37,24 +39,26 @@ The shared files prepended to this prompt define the layout recipes, model shape
37
39
  |---|---|
38
40
  | **System name/boundary** | Names the rectangle |
39
41
  | **Human actors** (roles) | External humans who interact with the system |
40
- | **System actors** (external systems) | Draw as «system» rectangle |
42
+ | **System actors** (external systems) | Drawn as a stick figure with `«system»` stereotype (not a rectangle) |
41
43
  | **Actor generalizations** | Child actor does everything parent does + more |
42
44
  | **System administrator** | Almost always present — manages master data for every entity |
43
45
  | **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 |
46
+ | **Shared mandatory sub-behaviors** | «include» candidates — must pass the §4 gate |
47
+ | **Optional/conditional behaviors** | «extend» candidates — must pass the §4 gate |
46
48
  | **Language for labels** | Hebrew, English, or bilingual |
47
49
 
48
50
  ---
49
51
 
50
52
  ## 2. Actor Rules
51
53
 
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
54
+ - **Human actor**: stick figure with name below.
55
+ - **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.
56
+ - **Positioning**: primary actors on the left, secondary/admin on the right, external systems that connect to only one UC below the boundary.
57
+ - **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
58
 
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).
59
+ **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).
60
+
61
+ **System-time / cron actor (שחקן זמן).** Draw as a system-style actor (amber head, `«system»` label). Every association line from a time actor MUST carry a visible timing label at the midpoint (e.g., `פעם ביום`, `כל 5 דקות`, `בסוף חודש`). Without a timing label the trigger frequency is ambiguous and the diagram is incomplete. Rendering: the label is injected as a `<g class="timing-label">` (white `<rect>` + `<text>`) after `fixLayout` sets the endpoint coordinates — see §8.
58
62
 
59
63
  ---
60
64
 
@@ -62,69 +66,89 @@ The shared files prepended to this prompt define the layout recipes, model shape
62
66
 
63
67
  **Granularity**: one complete user goal per UC, achievable in one session.
64
68
 
69
+ **Labels describe actions WITH the information system, not physical real-world actions.** A use case is the actor's interaction with the IS; the verb must imply data entry, query, or a system-state change.
70
+
71
+ - ✅ Preferred verbs: `רישום`, `הזנה`, `עדכון`, `הצגה`, `הפקה` (of a report), `שליחה` (of a notification), `אישור`, `הקצאה`, `חיפוש`, `סגירה`, `פתיחה` (of a record).
72
+ - ❌ Bare physical verbs that describe things happening outside the system: `הכנת משלוח` (physical preparing), `ספירת מלאי` (physical counting), `מסירת חבילה` (physical delivering), `תשלום במזומן` (physical paying).
73
+ - **Fix rule:** when the requirement uses a physical verb, wrap it with a system verb. `הכנת משלוח` → `רישום הכנת משלוח` or `סימון משלוח כמוכן`. `ביצוע ספירת מלאי` → `הזנת תוצאות ספירת מלאי`. `קבלת משלוח` → `קליטת משלוח נכנס` (data entry on arrival).
74
+
65
75
  **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)
76
+ - Simple entity (just a form): single "ניהול X" is fine.
77
+ - Complex entity with sub-items, lifecycle states, or different actors per operation: separate UCs (open / update / cancel / approve).
68
78
 
69
79
  **Implied UCs**: every entity the system references needs at least one UC that manages it.
70
80
 
71
81
  ---
72
82
 
73
- ## 4. Include and Extend
83
+ ## 4. Include and Extend — Mandatory Justification Gate
74
84
 
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.
85
+ **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
86
 
77
- ### «include»
78
- «include» means: every time the base UC runs, the included UC **always** runs as part of it — no exceptions, no conditions. It is not "if X happens, then Y" — that is «extend». It is "X always does Y as part of its execution."
87
+ ### «include» justification block
79
88
 
80
- However, **always-happens does not automatically mean extract**. A step that always happens inside a UC is usually just part of that UC. Only extract it when:
81
- 1. It is shared by **2 or more** base UCs — there must be ≥2 incoming «include» arrows
82
- 2. The included UC has **no direct association to any actor** — actor-connected UCs are standalone UCs, not sub-behaviors
83
- 3. Extracting it genuinely reduces duplication not just because it feels like a "step"
89
+ ```
90
+ «include» candidate: __________________________________
91
+ Base UC #1 (actor-initiated, runs target every time): __________
92
+ Base UC #2 (actor-initiated, runs target every time): __________
93
+ Target has zero direct actor associations? yes / no
94
+ Extracting reduces real duplication (not just "feels like a step")? yes / no
95
+
96
+ → If Base UC #2 is blank → DELETE the «include». Fold into Base UC #1.
97
+ → If actor-associations = yes → DELETE. The target is a regular UC, not a helper.
98
+ → If duplication reduction = no → DELETE. A step inside one UC is just a step.
99
+ ```
84
100
 
85
- Arrow direction: Base UC Included UC. Dashed line, open arrowhead.
101
+ Semantics: «include» means the target runs **every single time** the base runs, unconditionally. It is never "sometimes X". That is «extend».
102
+
103
+ Arrow: dashed, open arrowhead, `Base UC → Included UC`.
104
+
105
+ ### «extend» justification block
106
+
107
+ ```
108
+ «extend» candidate: __________________________________
109
+ Base UC being extended: __________
110
+ Exact trigger: "when __________ happens" (one sentence, specific condition)
111
+ Base UC runs correctly without the extension? yes / no
112
+ Is this behavior significant enough to model (not every trivial optional path)? yes / no
113
+
114
+ → If trigger is vague or cannot be stated in one sentence → DELETE.
115
+ → 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.
116
+ → If insignificant → DELETE. Not every optional step deserves «extend».
117
+ ```
86
118
 
87
- **Immediate disqualifiers:**
88
- - Only one base UC points to it → fold it into the parent UC
89
- - An actor connects to it → it is a regular standalone UC, not an «include» target
90
- - It is conditional or optional → that is «extend», not «include»
119
+ Arrow: dashed, open arrowhead, `Extension UC → Base UC`. Use at most once or twice per diagram.
91
120
 
92
- ### «extend»
93
- «extend» means: sometimes, under a specific condition, an additional behavior fires. The base UC runs fine without it.
121
+ ### Quick disqualifiers
94
122
 
95
- Arrow direction: Extension UC Base UC. Use at most once or twice per diagram. If you cannot state the exact condition in one sentence, do not add it.
123
+ - Exactly one incoming «include» arrow fold into the parent.
124
+ - Actor directly connects to an alleged «include» target → it is a standalone UC, not a sub-behavior.
125
+ - "Always happens as part of X" alone is not enough — it must also be shared by ≥2 bases.
126
+ - Conditional or optional behavior → this is «extend» territory, not «include».
96
127
 
97
128
  ---
98
129
 
99
130
  ## 5. Notation
100
131
 
101
- ### Actors
102
- ```
103
- Human: stick figure — circle (head) + lines (body/arms/legs), name below
104
- System: rectangle with «system» stereotype label, name inside
105
- Generalization: solid line, hollow triangle at parent end, L-shaped routing
106
- ```
132
+ 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.
133
+
134
+ ### Actor rendering
135
+ - Human: stick figure head circle + body/arms/legs lines, name below.
136
+ - System: same stick figure with amber head fill and `«system»` label line above the name.
137
+ - Generalization: solid line, hollow triangle at parent end, L-shaped routing.
107
138
 
108
139
  ### System boundary
109
140
  Large rounded rectangle (`rx=12`), title bar on top with system name (white text on `#1e40af` background).
110
141
 
111
142
  ### Use case ellipses
112
- - Horizontal ellipse: `rx` 70–125, `ry` 25–32 (size to fit text)
113
- - Fill: `#d4e9f7`, solid border `#2980b9` (1.8px) — **always solid, no exceptions**
114
- - Text: centered, 11px, may wrap 2 lines
115
-
116
- ### Association lines
117
- - Actor ↔ UC: solid line, **no arrowhead**
118
- - «include»/«extend»: dashed (dasharray 6 4), open arrowhead, `#7c3aed` stroke
119
- - Stereotype label (6 4 dashed line): 9.5px near midpoint
143
+ - Horizontal ellipse: `rx` 70–125, `ry` 25–32 (size to fit text).
144
+ - Fill `#d4e9f7`, solid border `#2980b9` (1.8px) — **always solid, no exceptions**.
145
+ - Text centered, 11px, may wrap to 2 lines.
120
146
 
121
147
  ### Ellipse endpoint calculation (mandatory)
122
-
123
- 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.
148
+ Never draw a line to the ellipse center — it disappears under the fill. Compute the boundary intersection:
124
149
 
125
150
  ```javascript
126
- // From shared layout-recipes.md:
127
- // ellipse-boundary-intersect(cx, cy, rx, ry, externalPoint)
151
+ // Shared layout-recipes.md: ellipse-boundary-intersect(cx, cy, rx, ry, externalPoint)
128
152
  angle = Math.atan2(externalPoint.y - cy, externalPoint.x - cx);
129
153
  endpoint = { x: cx + rx * Math.cos(angle), y: cy + ry * Math.sin(angle) };
130
154
  ```
@@ -133,149 +157,20 @@ endpoint = { x: cx + rx * Math.cos(angle), y: cy + ry * Math.sin(angle) };
133
157
  1. Background + dot grid
134
158
  2. System boundary rectangle
135
159
  3. Association lines (solid)
136
- 4. «include»/«extend» dashed lines with labels
160
+ 4. «include» / «extend» dashed lines with labels
137
161
  5. Generalization lines with triangles
138
162
  6. Use case ellipses (covers line endpoints — draw last among shapes)
139
163
  7. Actor figures and labels
140
164
 
141
- No legend. Do not add a legend bar to the diagram.
142
-
143
- ---
144
-
145
- ## 6. Layout Algorithm
146
-
147
- **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.
148
-
149
- ### Initial placement
150
-
151
- **Step 1 — Assign UCs to actor groups**
152
- 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).
153
-
154
- **Step 2 — Sort actors with `actor-adjacency-sort` (from shared layout-recipes.md)**
155
- Before placing actors, sort actors on each side by the barycentric Y of their UC groups to minimize line crossings.
156
-
157
- **Step 3 — Initial UC positions with `row-grid-pack`**
158
- Place UCs in each column using `row-grid-pack`:
159
- ```
160
- left column: x = boundary.x + boundary.width * 0.27
161
- right column: x = boundary.x + boundary.width * 0.73
162
- center column: x = boundary.x + boundary.width * 0.50
163
- rowHeight = max(ry * 2 + 80, 80)
164
- ```
165
-
166
- **Step 4 — Place actors at vertical center of their UC groups**
167
- ```javascript
168
- actor.y = average(actor.ucIds.map(id => useCases[id].cy));
169
- actor.x = side === 'left' ? boundary.x - 120 : boundary.x + boundary.width + 120;
170
- ```
171
-
172
- **Step 5 — Run `collision-nudge` on UCs** (from shared layout-recipes.md)
173
-
174
- **Step 6 — Run `validateLayout()` then `fixLayout()` on `DOMContentLoaded`**
175
-
176
- `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.
177
-
178
- ```javascript
179
- document.addEventListener('DOMContentLoaded', () => {
180
- validateLayout(part); // fix positions, expand boundary if needed
181
- applyLayout(svgId, part); // write to SVG
182
- });
183
- ```
184
-
185
- ---
186
-
187
- ## 7. Splitting Rules
188
-
189
- 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).
190
-
191
- **How to split:**
192
- - One tab per **actor group** (e.g., "מטופל" tab, "מנהל" tab) or functional area
193
- - Shared UCs appear in both tabs with a "(shared with Tab N)" badge
194
- - Each tab has its own complete SVG (its own boundary, actors, ellipses, lines)
195
- - Each tab has a visible **split rationale line**: `"Split by actor group: [actor names] — [N] UCs > 8 threshold"`
196
- - Run `fixLayout()` independently per tab on load
197
-
198
- For tabbed layouts: `diagramLayouts` array, one entry per tab.
199
-
200
- ---
201
-
202
- ## 8a. validateLayout — Boundary & Overlap Validation (Required)
203
-
204
- Run this **before** writing positions to the SVG. It is idempotent.
205
-
206
- ```javascript
207
- // Named constants — tune these, do not hardcode magic numbers elsewhere
208
- const HEADER_H = 44; // boundary header height in px
209
- const UC_PAD = 30; // min gap from boundary edge to nearest UC edge
210
- const UC_GAP = 14; // min gap between UC ellipses in the same column
211
-
212
- function validateLayout(part) {
213
- const b = part.boundary;
214
- const ucs = part.useCases;
215
-
216
- const minX = b.x + UC_PAD;
217
- const maxX = b.x + b.w - UC_PAD;
218
- const minY = b.y + HEADER_H + UC_PAD;
219
-
220
- let changed = true, iter = 0;
221
- while (changed && iter++ < 40) {
222
- changed = false;
223
-
224
- // 1. Clamp horizontal positions inside boundary
225
- for (const uc of ucs) {
226
- const nx = Math.max(minX + uc.rx, Math.min(maxX - uc.rx, uc.cx));
227
- if (nx !== uc.cx) { uc.cx = nx; changed = true; }
228
- }
229
-
230
- // 2. Push UCs below header
231
- for (const uc of ucs) {
232
- const ny = Math.max(minY + uc.ry, uc.cy);
233
- if (ny !== uc.cy) { uc.cy = ny; changed = true; }
234
- }
235
-
236
- // 3. Resolve vertical overlaps within each column
237
- const cols = {};
238
- for (const uc of ucs) {
239
- const col = uc.col || 'center';
240
- (cols[col] = cols[col] || []).push(uc);
241
- }
242
- for (const col of Object.values(cols)) {
243
- col.sort((a, b) => a.cy - b.cy);
244
- for (let i = 1; i < col.length; i++) {
245
- const prev = col[i-1], cur = col[i];
246
- const needed = prev.cy + prev.ry + UC_GAP + cur.ry;
247
- if (cur.cy < needed) { cur.cy = needed; changed = true; }
248
- }
249
- }
250
- }
251
-
252
- // 4. Expand boundary height to fit all UCs
253
- const maxBottom = Math.max(...ucs.map(uc => uc.cy + uc.ry));
254
- const requiredH = maxBottom - b.y + UC_PAD;
255
- if (requiredH > b.h) b.h = requiredH;
256
-
257
- // 5. Re-center left/right actors on their UC groups
258
- for (const actor of part.actors) {
259
- if (actor.side === 'left' || actor.side === 'right') {
260
- const myUCIds = part.connections.filter(c => c.from === actor.id).map(c => c.to);
261
- const myUCs = ucs.filter(uc => myUCIds.includes(uc.id));
262
- if (myUCs.length) actor.y = myUCs.reduce((s, uc) => s + uc.cy, 0) / myUCs.length;
263
- }
264
- // Push bottom actors below expanded boundary
265
- if (actor.side === 'bottom') actor.y = b.y + b.h + 80;
266
- }
267
- }
268
- ```
269
-
270
- **When to call**: always before `applyLayout`. Both on `DOMContentLoaded` and when the Fix Layout button is clicked.
165
+ **No legend.** Do not add a legend bar to the diagram.
271
166
 
272
167
  ---
273
168
 
274
- ## 8. Fix Layout Algorithm (Primary Placement Path)
169
+ ## 6. Layout
275
170
 
276
- 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.
171
+ 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.
277
172
 
278
- ### Required data structure
173
+ ### Data structure
279
174
 
280
175
  ```javascript
281
176
  const diagramLayout = {
@@ -298,21 +193,27 @@ const diagramLayout = {
298
193
  };
299
194
  ```
300
195
 
301
- ### SVG element conventions (required for fix algorithm)
196
+ ### SVG element conventions (required)
302
197
 
303
- - **UC groups**: `<g id="uc-group-{ucId}">` with `<ellipse cx="..." cy="...">` (NOT transforms for UC positioning)
304
- - **Actor groups**: `<g id="actor-group-{actorId}" transform="translate(x,y)">`
305
- - **Connection lines**: `<line data-conn-from="{id}" data-conn-to="{id}" x1 y1 x2 y2>`
198
+ - UC groups: `<g id="uc-group-{ucId}">` with `<ellipse cx="..." cy="...">` — do NOT use `transform` to position UCs.
199
+ - Actor groups: `<g id="actor-group-{actorId}" transform="translate(x,y)">`.
200
+ - Connection lines: `<line data-conn-from="{id}" data-conn-to="{id}" x1 y1 x2 y2>`.
306
201
 
307
- ### Fix algorithm
202
+ ### fixLayout implementation
308
203
 
309
204
  ```javascript
205
+ // Named constants — tune these, do not hardcode magic numbers elsewhere.
206
+ const HEADER_H = 44; // boundary header height
207
+ const UC_PAD = 30; // min gap from boundary edge to nearest UC edge
208
+ const UC_GAP = 14; // min gap between UC ellipses in the same column
209
+ const ACTOR_PAD = 80; // horizontal gap between boundary and actor column
210
+
310
211
  function fixLayout(layout) {
311
212
  const svg = document.getElementById(layout.svgId);
312
213
  if (!svg) return;
313
- const pad = 80;
214
+ const b = layout.boundary;
314
215
 
315
- // Step 1: Group UCs by actor side
216
+ // Step 1 Group UCs by the side of their owning actor.
316
217
  const ucSide = {};
317
218
  layout.actors.forEach(a =>
318
219
  a.ucIds.forEach(uid => {
@@ -322,41 +223,82 @@ function fixLayout(layout) {
322
223
  const cols = { left: [], right: [], center: [] };
323
224
  layout.useCases.forEach(uc => cols[ucSide[uc.id] || 'center'].push(uc));
324
225
 
325
- // Step 2: Sort each column by actor order (top-to-bottom on that side)
226
+ // Step 2 Sort each column by actor order (top-to-bottom on that side).
326
227
  function sortCol(ucs, side) {
327
- const ordered = layout.actors.filter(a => a.side === side).sort((a,b) => a.y - b.y);
328
- ucs.sort((a, b) => {
228
+ const ordered = layout.actors.filter(a => a.side === side).sort((a, bb) => a.y - bb.y);
229
+ ucs.sort((a, bb) => {
329
230
  const ai = ordered.findIndex(act => act.ucIds.includes(a.id));
330
- const bi = ordered.findIndex(act => act.ucIds.includes(b.id));
231
+ const bi = ordered.findIndex(act => act.ucIds.includes(bb.id));
331
232
  return (ai < 0 ? 999 : ai) - (bi < 0 ? 999 : bi);
332
233
  });
333
234
  }
334
- sortCol(cols.left, 'left');
235
+ sortCol(cols.left, 'left');
335
236
  sortCol(cols.right, 'right');
336
237
 
337
- // Step 3: Assign Y positions
338
- const bx = layout.boundary.x, bw = layout.boundary.width, by = layout.boundary.y;
339
- function place(ucs, colX) {
238
+ // Step 3 Adaptive column X positions. Account for actual ellipse widths so
239
+ // adjacent columns never overlap horizontally. Widen the boundary if needed.
240
+ const COL_GAP = 30;
241
+ const maxRx = ucs => (ucs.length ? Math.max(...ucs.map(u => u.rx)) : 0);
242
+ const lRx = maxRx(cols.left), cRx = maxRx(cols.center), rRx = maxRx(cols.right);
243
+ const gaps =
244
+ (cRx ? 2 * COL_GAP : (lRx && rRx ? 2 * COL_GAP : 0));
245
+ const needed = 2 * UC_PAD + 2 * lRx + 2 * cRx + 2 * rRx + gaps;
246
+ if (b.width < needed) b.width = needed;
247
+
248
+ const leftX = b.x + UC_PAD + lRx;
249
+ const rightX = b.x + b.width - UC_PAD - rRx;
250
+ const centerX = (leftX + rightX) / 2;
251
+
252
+ function place(ucs, colX, colName) {
340
253
  ucs.forEach((uc, i) => {
341
- uc.cy = by + 70 + i * (uc.ry * 2 + pad) + uc.ry;
254
+ uc.cy = b.y + HEADER_H + UC_PAD + uc.ry + i * (uc.ry * 2 + UC_GAP);
342
255
  uc.cx = colX;
256
+ uc.col = colName;
343
257
  });
344
258
  }
345
- place(cols.left, bx + bw * 0.28);
346
- place(cols.right, bx + bw * 0.72);
347
- place(cols.center, bx + bw * 0.50);
259
+ place(cols.left, leftX, 'left');
260
+ place(cols.right, rightX, 'right');
261
+ place(cols.center, centerX, 'center');
262
+
263
+ // Step 4 — Clamp and resolve overlaps. Idempotent fixed-point loop.
264
+ const minX = b.x + UC_PAD;
265
+ const maxX = b.x + b.width - UC_PAD;
266
+ const minY = b.y + HEADER_H + UC_PAD;
267
+ let changed = true, iter = 0;
268
+ while (changed && iter++ < 40) {
269
+ changed = false;
270
+ for (const uc of layout.useCases) {
271
+ const nx = Math.max(minX + uc.rx, Math.min(maxX - uc.rx, uc.cx));
272
+ if (nx !== uc.cx) { uc.cx = nx; changed = true; }
273
+ const ny = Math.max(minY + uc.ry, uc.cy);
274
+ if (ny !== uc.cy) { uc.cy = ny; changed = true; }
275
+ }
276
+ for (const col of Object.values(cols)) {
277
+ col.sort((a, bb) => a.cy - bb.cy);
278
+ for (let i = 1; i < col.length; i++) {
279
+ const prev = col[i - 1], cur = col[i];
280
+ const needed = prev.cy + prev.ry + UC_GAP + cur.ry;
281
+ if (cur.cy < needed) { cur.cy = needed; changed = true; }
282
+ }
283
+ }
284
+ }
348
285
 
349
- // Step 4: Recenter actors on their UC groups
286
+ // Step 5 Expand boundary height to fit all UCs.
287
+ const maxBottom = Math.max(...layout.useCases.map(uc => uc.cy + uc.ry));
288
+ b.height = Math.max(b.height, maxBottom - b.y + UC_PAD);
289
+
290
+ // Step 6 — Re-center actors on their UC group; push bottom actors below boundary.
350
291
  layout.actors.forEach(actor => {
351
292
  const myUCs = layout.useCases.filter(u => actor.ucIds.includes(u.id));
352
- if (myUCs.length) actor.y = myUCs.reduce((s,u) => s + u.cy, 0) / myUCs.length;
293
+ if (actor.side === 'left' || actor.side === 'right') {
294
+ if (myUCs.length) actor.y = myUCs.reduce((s, u) => s + u.cy, 0) / myUCs.length;
295
+ actor.x = actor.side === 'left' ? b.x - ACTOR_PAD : b.x + b.width + ACTOR_PAD;
296
+ } else if (actor.side === 'bottom') {
297
+ actor.y = b.y + b.height + ACTOR_PAD;
298
+ }
353
299
  });
354
300
 
355
- // Step 5: Expand boundary height if needed
356
- const maxY = Math.max(...layout.useCases.map(u => u.cy + u.ry));
357
- layout.boundary.height = Math.max(layout.boundary.height, maxY - by + 60);
358
-
359
- // Step 6: Apply positions — absolute attributes only, never cumulative
301
+ // Step 7 Write UC positions to the SVG (absolute attributes; translate deltas on text).
360
302
  layout.useCases.forEach(uc => {
361
303
  const g = document.getElementById('uc-group-' + uc.id);
362
304
  if (!g) return;
@@ -377,7 +319,7 @@ function fixLayout(layout) {
377
319
  if (g) g.setAttribute('transform', `translate(${actor.x},${actor.y})`);
378
320
  });
379
321
 
380
- // Step 7: Recalculate all line endpoints using ellipse-boundary-intersect
322
+ // Step 8 Recalculate every connection line endpoint.
381
323
  svg.querySelectorAll('[data-conn-from][data-conn-to]').forEach(line => {
382
324
  const fId = line.getAttribute('data-conn-from');
383
325
  const tId = line.getAttribute('data-conn-to');
@@ -385,25 +327,28 @@ function fixLayout(layout) {
385
327
  const fAc = layout.actors.find(a => a.id === fId);
386
328
  const tUC = layout.useCases.find(u => u.id === tId);
387
329
  const tAc = layout.actors.find(a => a.id === tId);
388
- const from = fAc ? {x: fAc.x, y: fAc.y} : fUC ? {x: fUC.cx, y: fUC.cy} : null;
389
- const to = tAc ? {x: tAc.x, y: tAc.y} : tUC ? {x: tUC.cx, y: tUC.cy} : null;
330
+ const from = fAc ? { x: fAc.x, y: fAc.y } : fUC ? { x: fUC.cx, y: fUC.cy } : null;
331
+ const to = tAc ? { x: tAc.x, y: tAc.y } : tUC ? { x: tUC.cx, y: tUC.cy } : null;
390
332
  if (!from || !to) return;
391
333
  let x1 = from.x, y1 = from.y, x2 = to.x, y2 = to.y;
392
- 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); }
393
- 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); }
394
- line.setAttribute('x1',x1); line.setAttribute('y1',y1);
395
- line.setAttribute('x2',x2); line.setAttribute('y2',y2);
334
+ 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); }
335
+ 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); }
336
+ line.setAttribute('x1', x1); line.setAttribute('y1', y1);
337
+ line.setAttribute('x2', x2); line.setAttribute('y2', y2);
396
338
  });
397
339
 
398
- // Step 8: Resize boundary rect + viewBox
340
+ // Step 9 Resize boundary rect and SVG viewBox.
399
341
  const br = svg.querySelector('.system-boundary, rect[rx="12"]');
400
- if (br) br.setAttribute('height', layout.boundary.height);
342
+ if (br) br.setAttribute('height', b.height);
401
343
  const vb = svg.getAttribute('viewBox');
402
344
  if (vb) {
403
345
  const p = vb.split(/\s+/);
404
- p[3] = Math.max(parseFloat(p[3]), by + layout.boundary.height + 80);
346
+ p[3] = Math.max(parseFloat(p[3]), b.y + b.height + 120);
405
347
  svg.setAttribute('viewBox', p.join(' '));
406
348
  }
349
+
350
+ // Step 10 — Decorate timed associations (see §8 renderTimingLabels).
351
+ if (typeof renderTimingLabels === 'function') renderTimingLabels(layout);
407
352
  }
408
353
  ```
409
354
 
@@ -424,9 +369,39 @@ function fixLayout(layout) {
424
369
 
425
370
  ---
426
371
 
427
- ## 9. Clickable Descriptions for UCs and Actors (Always Include)
372
+ ## 7. Splitting Into Multiple Diagrams
373
+
374
+ Split the use case diagram into multiple tabs in one HTML file when **either** trigger fires:
375
+
376
+ 1. **Total UC count > 8.**
377
+ 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).
378
+
379
+ If neither trigger fires, produce a single tab. Do not split on gut feeling.
380
+
381
+ ### Examples
382
+
383
+ | Scenario | UC count | Actor groups | Split? | Reason |
384
+ |---|---|---|---|---|
385
+ | Small library system | 5 | 1 (reader) | **No** | Both triggers fail |
386
+ | Coffee shop | 7 | 2 (customer 4 UCs, barista 3 UCs) disjoint | **Yes** | Trigger #2 fires |
387
+ | Hospital clinic | 12 | 1 (doctor) owns most | **Yes** | Trigger #1 fires |
388
+ | Course registration | 8 | 2 (student 5, admin 3) — share "login" only | **Yes** | Trigger #2 fires (login handled via cross-tab stub) |
389
+
390
+ ### Mechanics
391
+
392
+ - One tab per actor group or per functional area.
393
+ - **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.
394
+ - Each tab has its own complete SVG (its own boundary, actors, ellipses, lines).
395
+ - Each tab has a visible **split-rationale line** below the tab bar. Use whichever trigger actually fired:
396
+ - `"Split by UC count: 12 UCs > 8 threshold"` or
397
+ - `"Split by actor groups: [group A] / [group B] — disjoint responsibilities"`.
398
+ - 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.
399
+
400
+ ---
401
+
402
+ ## 8. Clickable Descriptions for UCs and Actors (Always Include)
428
403
 
429
- 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.
404
+ 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.
430
405
 
431
406
  ```javascript
432
407
  const useCaseDescriptions = {
@@ -469,27 +444,71 @@ document.addEventListener('click', e => {
469
444
  ```
470
445
 
471
446
  UC group: `<g id="uc-uc1" data-uc-id="uc1" onclick="showDesc('uc1','uc',event)" style="cursor:pointer">`
472
-
473
447
  Actor group: `<g id="actor-student" data-actor-id="student" onclick="showDesc('student','actor',event)" style="cursor:pointer">`
474
448
 
475
- **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.
449
+ ### Timing labels on cron/time-actor associations (required)
450
+
451
+ When a connection carries a `timing` field (every connection from a system-time actor MUST have one — see §2), inject the label at the line midpoint **after** `fixLayout` has written the endpoints. The helper below is idempotent: it removes any existing `.timing-label` group before re-rendering, so calling it again after another `fixLayout` pass is safe.
452
+
453
+ ```javascript
454
+ function renderTimingLabels(layout) {
455
+ const svg = document.getElementById(layout.svgId);
456
+ if (!svg) return;
457
+ svg.querySelectorAll('.timing-label').forEach(el => el.remove());
458
+ const NS = 'http://www.w3.org/2000/svg';
459
+ (layout.connections || []).forEach(conn => {
460
+ if (!conn.timing) return;
461
+ const line = svg.querySelector(
462
+ `[data-conn-from="${conn.from}"][data-conn-to="${conn.to}"]`);
463
+ if (!line) return;
464
+ const x1 = +line.getAttribute('x1'), y1 = +line.getAttribute('y1');
465
+ const x2 = +line.getAttribute('x2'), y2 = +line.getAttribute('y2');
466
+ const mx = (x1 + x2) / 2, my = (y1 + y2) / 2;
467
+ const w = 10 + conn.timing.length * 7, h = 16;
468
+ const g = document.createElementNS(NS, 'g');
469
+ g.setAttribute('class', 'timing-label');
470
+ const rect = document.createElementNS(NS, 'rect');
471
+ rect.setAttribute('x', mx - w / 2); rect.setAttribute('y', my - h / 2);
472
+ rect.setAttribute('width', w); rect.setAttribute('height', h);
473
+ rect.setAttribute('fill', 'white'); rect.setAttribute('stroke', '#334155');
474
+ rect.setAttribute('stroke-width', '0.8'); rect.setAttribute('rx', '3');
475
+ const text = document.createElementNS(NS, 'text');
476
+ text.setAttribute('x', mx); text.setAttribute('y', my + 4);
477
+ text.setAttribute('font-size', '10'); text.setAttribute('text-anchor', 'middle');
478
+ text.setAttribute('fill', '#334155');
479
+ text.setAttribute('font-family', "'Noto Sans Hebrew', sans-serif");
480
+ text.setAttribute('direction', 'rtl');
481
+ text.textContent = conn.timing;
482
+ g.appendChild(rect); g.appendChild(text);
483
+ svg.appendChild(g);
484
+ });
485
+ }
486
+ ```
487
+
488
+ Call `renderTimingLabels(layout)` at the end of every `fixLayout` invocation, **and** on `DOMContentLoaded` (after the initial `fixLayout`). Add it inside `showDiagram(index)` too so the label tracks the active tab.
489
+
490
+ The `connections` entry for a timed link looks like:
491
+
492
+ ```javascript
493
+ { from: 'cron', to: 'uc-daily-reconcile', type: 'association', timing: 'פעם ביום' }
494
+ ```
476
495
 
477
496
  ---
478
497
 
479
- ## 10. diagramModel (mandatory)
498
+ ## 9. diagramModel (mandatory)
480
499
 
481
- 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.
500
+ 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.
482
501
 
483
502
  ---
484
503
 
485
- ## 11. Styling
504
+ ## 10. Styling
486
505
 
487
506
  ```
488
507
  Background: #f8fafc, dot-grid (24px, #cbd5e1, r=0.8)
489
508
  System boundary: fill white, stroke #3b82f6 (2.5px), header fill #1e40af, rx=12
490
509
  Ellipse: fill #d4e9f7, stroke #2980b9 (1.8px), solid
491
510
  Ellipse text: #1a1a2e, 11px, weight 500, text-anchor middle
492
- Actor stick figure: stroke #334155 (2px), head fill #e0f0ff
511
+ Actor stick figure: stroke #334155 (2px), human head fill #e0f0ff, system head fill #fef3c7
493
512
  Actor label: #1e293b, 11px, weight 600, text-anchor middle
494
513
  Association line: #334155, 1.8px, no arrowhead
495
514
  Include/extend: #7c3aed, 1.6px, dasharray 6 4, open arrowhead, label 9.5px
@@ -500,7 +519,7 @@ Fonts: 'Noto Sans Hebrew' + 'IBM Plex Mono' from Google Fonts
500
519
 
501
520
  ---
502
521
 
503
- ## 12. HTML File Structure
522
+ ## 11. HTML File Structure
504
523
 
505
524
  ```html
506
525
  <!DOCTYPE html>
@@ -513,86 +532,87 @@ Fonts: 'Noto Sans Hebrew' + 'IBM Plex Mono' from Google Fonts
513
532
  </head>
514
533
  <body>
515
534
  <!-- Tab bar (only when split) -->
516
- <div class="tab-bar">...</div>
535
+ <div class="tab-bar">
536
+ <button class="tab active" onclick="showDiagram(0)">[Tab 1 label]</button>
537
+ <button class="tab" onclick="showDiagram(1)">[Tab 2 label]</button>
538
+ </div>
539
+
540
+ <!-- Split rationale (only when split) -->
541
+ <div class="split-rationale">Split by UC count: N UCs &gt; 8 threshold</div>
517
542
 
518
543
  <!-- Fix Layout button (above each diagram) -->
519
544
  <div style="text-align:center;margin:8px 0;">
520
- <button onclick="fixLayout(diagramLayout)" class="fix-btn">Fix Layout</button>
545
+ <button onclick="fixLayout(currentLayout())" class="fix-btn">Fix Layout</button>
521
546
  </div>
522
547
 
523
- <!-- Split rationale (only when split) -->
524
- <div class="split-rationale">Split by actor group: [...] — N UCs > 8 threshold</div>
525
-
526
- <!-- Diagram SVG -->
527
- <div id="diagram-0">
528
- <svg id="diagram-svg" viewBox="0 0 1100 900" xmlns="http://www.w3.org/2000/svg">
548
+ <!-- One container per tab; inactive tabs are hidden. -->
549
+ <div class="diagram-part" id="diagram-0">
550
+ <svg id="diagram-svg-0" viewBox="0 0 1100 900" xmlns="http://www.w3.org/2000/svg">
529
551
  <!-- dot grid, boundary, association lines, include/extend lines,
530
552
  generalization lines, UC ellipse groups, actor groups -->
531
553
  </svg>
532
554
  </div>
555
+ <div class="diagram-part" id="diagram-1" hidden>
556
+ <svg id="diagram-svg-1" viewBox="0 0 1100 900" xmlns="http://www.w3.org/2000/svg">...</svg>
557
+ </div>
533
558
 
534
559
  <!-- VP export button -->
535
560
  <button class="vp-export-btn" onclick="exportXMI()">Download .xmi (Visual Paradigm)</button>
536
561
 
537
562
  <script>
538
- const diagramLayout = { ... }; // §8 data structure
539
- const useCaseDescriptions = { ... }; // §9
540
- const diagramModel = { ... }; // model-shape.md for kind:'use-case'
541
- function fixLayout(layout) { ... } // §8 algorithm
542
- function showDescription(id, e) { ... } // §9
543
- function hideDescription() { ... }
544
- function xmiBody(elements, rels) { ... } // export-buttons.md
545
- function exportXMI() { ... }
546
- function xmlEscape(s) { ... }
547
- function downloadFile(...) { ... }
548
- // Run layout on page load:
549
- document.addEventListener('DOMContentLoaded', () => fixLayout(diagramLayout));
563
+ const diagramLayouts = [ /* one layout object per tab, per §6 */ ];
564
+ const useCaseDescriptions = { /* §8 */ };
565
+ const actorDescriptions = { /* §8 */ };
566
+ const diagramModel = { /* model-shape.md, kind: 'use-case' */ };
567
+
568
+ let activeTab = 0;
569
+ function currentLayout() { return diagramLayouts[activeTab]; }
570
+ function showDiagram(index) {
571
+ activeTab = index;
572
+ document.querySelectorAll('.diagram-part').forEach((p, i) => p.hidden = i !== index);
573
+ document.querySelectorAll('.tab').forEach((t, i) => t.classList.toggle('active', i === index));
574
+ fixLayout(diagramLayouts[index]); // idempotent
575
+ }
576
+
577
+ function fixLayout(layout) { /* §6 */ }
578
+ function showDesc(id, type, event) { /* §8 */ }
579
+ function hideDesc() { /* §8 */ }
580
+ function xmiBody(elements, rels) { /* export-buttons.md */ }
581
+ function exportXMI() { /* export-buttons.md */ }
582
+
583
+ document.addEventListener('DOMContentLoaded', () => {
584
+ diagramLayouts.forEach(l => fixLayout(l));
585
+ });
550
586
  </script>
551
587
  </body>
552
588
  </html>
553
589
  ```
554
590
 
555
- For **tabbed layouts**, add the `showDiagram(index)` function and run `fixLayout(diagramLayouts[i])` for each tab index on `DOMContentLoaded`.
591
+ 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.
556
592
 
557
593
  ---
558
594
 
559
- ## 13. Output
595
+ ## 12. Output
560
596
 
561
- - Save as `[system_name]_use_case_diagram.html`
562
- - Briefly state: actor count, use case count, include/extend relationships, whether split (and why)
597
+ - Save as `[system_name]_use_case_diagram.html`.
598
+ - Briefly state: actor count, use case count, include/extend relationships (with the justification line for each), whether split (and which trigger fired).
563
599
 
564
600
  ---
565
601
 
566
- ## 14. Pre-Delivery Checklist
567
-
568
- - [ ] Every Hebrew label uses male form אח not אחות, מזכיר not מזכירה, מטופל not מטופלת
569
- - [ ] No generic "ניהול הגדרות מערכת" or combined-entity UCs
570
- - [ ] All ellipses have solid borders (zero dashed ellipses)
571
- - [ ] Every «include» has ≥2 incoming arrows; included UC has no direct actor association
572
- - [ ] Every UC has ≥1 association line to an actor (no orphaned ellipses)
573
- - [ ] Actors ≥200px vertical distance from each other
574
- - [ ] SVG draw order: boundary lines ellipses → actors
575
- - [ ] `validateLayout()` runs before `applyLayout()` both on load and on Fix Layout click
576
- - [ ] No UC overlaps the header bar (uc.cy - uc.ry >= boundary.y + HEADER_H + UC_PAD)
577
- - [ ] No two UCs in the same column overlap each other (gap >= UC_GAP)
578
- - [ ] Boundary height auto-expanded if UCs exceed initial height
579
- - [ ] SVG viewBox large enough to show all actors including bottom actors
580
- - [ ] Ellipse endpoints computed via `ellipsePt` (not drawn to center)
581
- - [ ] UC description popups wired to every `data-uc-id` element
582
- - [ ] Actor description popups wired to every `data-actor-id` element
583
- - [ ] System actors drawn as stick figures with `«system»` italic label (not rectangles)
584
- - [ ] Bottom actors (external systems connecting only below) positioned below boundary
585
- - [ ] If split: each tab has a split-rationale line; layout applied independently per tab
586
- - [ ] `diagramModel` defined per model-shape.md
587
- - [ ] VP export button present and wired to `exportXMI()`
588
-
589
- ---
590
-
591
- ## 15. Hebrew Language Guidelines
592
-
593
- Always use **male grammatical form** (זכר) for all Hebrew actor and role names.
594
-
595
- ✅ רופא, אח, מטופל, לקוח, מנהל, עובד, משתמש, סטודנט, מרצה
596
- ❌ אחות, מטופלת, לקוחה, מנהלת, מזכירה, סטודנטית
597
-
598
- When in doubt, use the male form.
602
+ ## 13. Pre-Delivery Checklist
603
+
604
+ - [ ] Every «include» and «extend» has a written §4 justification block; any that cannot be filled in has been deleted.
605
+ - [ ] No «include» with exactly one incoming arrow — every include target has ≥2 bases AND zero actor associations.
606
+ - [ ] No generic "ניהול הגדרות מערכת" or combined-entity UCs; each entity has its own UC.
607
+ - [ ] Every UC has ≥1 association line to an actor (no orphaned ellipses).
608
+ - [ ] All ellipses have solid borders (zero dashed ellipses).
609
+ - [ ] SVG draw order: boundary lines → ellipses → actors.
610
+ - [ ] Ellipse endpoints computed via boundary intersection (not drawn to center).
611
+ - [ ] `fixLayout` runs on `DOMContentLoaded` and on the Fix Layout button; uses named constants (HEADER_H, UC_PAD, UC_GAP, ACTOR_PAD), not magic numbers.
612
+ - [ ] UC and actor description popups wired to every `data-uc-id` / `data-actor-id` element.
613
+ - [ ] System actors drawn as stick figures with `«system»` label (not rectangles).
614
+ - [ ] Every UC label describes an action WITH the information system (data entry, query, state change) — no bare physical verbs (`הכנת X`, `ספירת X`, `מסירת X`, `תשלום במזומן`). Physical verbs are wrapped with a system verb (`רישום`, `הזנה`, `עדכון`, `סימון`, etc.).
615
+ - [ ] Every system-time / cron actor association has a `timing` field (and the rendered diagram shows a visible label at the line midpoint) — no unlabeled cron connections.
616
+ - [ ] `fixLayout` uses adaptive column X (widens boundary if needed so left/center/right ellipses never overlap horizontally).
617
+ - [ ] 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.
618
+ - [ ] `diagramModel` defined per `model-shape.md`; VP export button present and wired to `exportXMI()`.