sad-mcp 1.1.2 → 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 +1 -1
- package/skills/uml-use-case-diagram/SKILL.md +228 -282
package/package.json
CHANGED
|
@@ -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 אחות)
|
|
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»
|
|
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:
|
|
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) |
|
|
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
|
|
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
|
|
54
|
-
- **
|
|
55
|
-
- **Generalization**: solid line, hollow triangle at parent end. Route as **L-shape** (horizontal away from boundary, then vertical, then back) — never a
|
|
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** —
|
|
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,68 +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
|
-
**
|
|
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
|
-
«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."
|
|
77
|
+
### «include» justification block
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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`.
|
|
84
94
|
|
|
85
|
-
|
|
95
|
+
### «extend» justification block
|
|
86
96
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
```
|
|
91
108
|
|
|
92
|
-
|
|
93
|
-
«extend» means: sometimes, under a specific condition, an additional behavior fires. The base UC runs fine without it.
|
|
109
|
+
Arrow: dashed, open arrowhead, `Extension UC → Base UC`. Use at most once or twice per diagram.
|
|
94
110
|
|
|
95
|
-
|
|
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».
|
|
96
117
|
|
|
97
118
|
---
|
|
98
119
|
|
|
99
120
|
## 5. Notation
|
|
100
121
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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.
|
|
107
128
|
|
|
108
129
|
### System boundary
|
|
109
130
|
Large rounded rectangle (`rx=12`), title bar on top with system name (white text on `#1e40af` background).
|
|
110
131
|
|
|
111
132
|
### Use case ellipses
|
|
112
|
-
- Horizontal ellipse: `rx` 70–125, `ry` 25–32 (size to fit text)
|
|
113
|
-
- Fill
|
|
114
|
-
- Text
|
|
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
|
|
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.
|
|
120
136
|
|
|
121
137
|
### 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.
|
|
138
|
+
Never draw a line to the ellipse center — it disappears under the fill. Compute the boundary intersection:
|
|
124
139
|
|
|
125
140
|
```javascript
|
|
126
|
-
//
|
|
127
|
-
// ellipse-boundary-intersect(cx, cy, rx, ry, externalPoint)
|
|
141
|
+
// Shared layout-recipes.md: ellipse-boundary-intersect(cx, cy, rx, ry, externalPoint)
|
|
128
142
|
angle = Math.atan2(externalPoint.y - cy, externalPoint.x - cx);
|
|
129
143
|
endpoint = { x: cx + rx * Math.cos(angle), y: cy + ry * Math.sin(angle) };
|
|
130
144
|
```
|
|
@@ -133,149 +147,20 @@ endpoint = { x: cx + rx * Math.cos(angle), y: cy + ry * Math.sin(angle) };
|
|
|
133
147
|
1. Background + dot grid
|
|
134
148
|
2. System boundary rectangle
|
|
135
149
|
3. Association lines (solid)
|
|
136
|
-
4. «include
|
|
150
|
+
4. «include» / «extend» dashed lines with labels
|
|
137
151
|
5. Generalization lines with triangles
|
|
138
152
|
6. Use case ellipses (covers line endpoints — draw last among shapes)
|
|
139
153
|
7. Actor figures and labels
|
|
140
154
|
|
|
141
|
-
No legend
|
|
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
|
-
```
|
|
155
|
+
**No legend.** Do not add a legend bar to the diagram.
|
|
184
156
|
|
|
185
157
|
---
|
|
186
158
|
|
|
187
|
-
##
|
|
159
|
+
## 6. Layout
|
|
188
160
|
|
|
189
|
-
|
|
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.
|
|
190
162
|
|
|
191
|
-
|
|
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.
|
|
271
|
-
|
|
272
|
-
---
|
|
273
|
-
|
|
274
|
-
## 8. Fix Layout Algorithm (Primary Placement Path)
|
|
275
|
-
|
|
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.
|
|
277
|
-
|
|
278
|
-
### Required data structure
|
|
163
|
+
### Data structure
|
|
279
164
|
|
|
280
165
|
```javascript
|
|
281
166
|
const diagramLayout = {
|
|
@@ -298,21 +183,27 @@ const diagramLayout = {
|
|
|
298
183
|
};
|
|
299
184
|
```
|
|
300
185
|
|
|
301
|
-
### SVG element conventions (required
|
|
186
|
+
### SVG element conventions (required)
|
|
302
187
|
|
|
303
|
-
-
|
|
304
|
-
-
|
|
305
|
-
-
|
|
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>`.
|
|
306
191
|
|
|
307
|
-
###
|
|
192
|
+
### fixLayout implementation
|
|
308
193
|
|
|
309
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
|
+
|
|
310
201
|
function fixLayout(layout) {
|
|
311
202
|
const svg = document.getElementById(layout.svgId);
|
|
312
203
|
if (!svg) return;
|
|
313
|
-
const
|
|
204
|
+
const b = layout.boundary;
|
|
314
205
|
|
|
315
|
-
// Step 1
|
|
206
|
+
// Step 1 — Group UCs by the side of their owning actor.
|
|
316
207
|
const ucSide = {};
|
|
317
208
|
layout.actors.forEach(a =>
|
|
318
209
|
a.ucIds.forEach(uid => {
|
|
@@ -322,41 +213,69 @@ function fixLayout(layout) {
|
|
|
322
213
|
const cols = { left: [], right: [], center: [] };
|
|
323
214
|
layout.useCases.forEach(uc => cols[ucSide[uc.id] || 'center'].push(uc));
|
|
324
215
|
|
|
325
|
-
// Step 2
|
|
216
|
+
// Step 2 — Sort each column by actor order (top-to-bottom on that side).
|
|
326
217
|
function sortCol(ucs, side) {
|
|
327
|
-
const ordered = layout.actors.filter(a => a.side === side).sort((a,
|
|
328
|
-
ucs.sort((a,
|
|
218
|
+
const ordered = layout.actors.filter(a => a.side === side).sort((a, bb) => a.y - bb.y);
|
|
219
|
+
ucs.sort((a, bb) => {
|
|
329
220
|
const ai = ordered.findIndex(act => act.ucIds.includes(a.id));
|
|
330
|
-
const bi = ordered.findIndex(act => act.ucIds.includes(
|
|
221
|
+
const bi = ordered.findIndex(act => act.ucIds.includes(bb.id));
|
|
331
222
|
return (ai < 0 ? 999 : ai) - (bi < 0 ? 999 : bi);
|
|
332
223
|
});
|
|
333
224
|
}
|
|
334
|
-
sortCol(cols.left,
|
|
225
|
+
sortCol(cols.left, 'left');
|
|
335
226
|
sortCol(cols.right, 'right');
|
|
336
227
|
|
|
337
|
-
// Step 3
|
|
338
|
-
const bx = layout.boundary.x, bw = layout.boundary.width, by = layout.boundary.y;
|
|
228
|
+
// Step 3 — Initial Y assignment per column.
|
|
339
229
|
function place(ucs, colX) {
|
|
340
230
|
ucs.forEach((uc, i) => {
|
|
341
|
-
uc.cy =
|
|
231
|
+
uc.cy = b.y + HEADER_H + UC_PAD + uc.ry + i * (uc.ry * 2 + UC_GAP);
|
|
342
232
|
uc.cx = colX;
|
|
233
|
+
uc.col = ucSide[uc.id] || 'center';
|
|
343
234
|
});
|
|
344
235
|
}
|
|
345
|
-
place(cols.left,
|
|
346
|
-
place(cols.right,
|
|
347
|
-
place(cols.center,
|
|
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
|
+
}
|
|
348
262
|
|
|
349
|
-
// Step
|
|
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.
|
|
350
268
|
layout.actors.forEach(actor => {
|
|
351
269
|
const myUCs = layout.useCases.filter(u => actor.ucIds.includes(u.id));
|
|
352
|
-
if (
|
|
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
|
+
}
|
|
353
276
|
});
|
|
354
277
|
|
|
355
|
-
// Step
|
|
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
|
|
278
|
+
// Step 7 — Write UC positions to the SVG (absolute attributes; translate deltas on text).
|
|
360
279
|
layout.useCases.forEach(uc => {
|
|
361
280
|
const g = document.getElementById('uc-group-' + uc.id);
|
|
362
281
|
if (!g) return;
|
|
@@ -377,7 +296,7 @@ function fixLayout(layout) {
|
|
|
377
296
|
if (g) g.setAttribute('transform', `translate(${actor.x},${actor.y})`);
|
|
378
297
|
});
|
|
379
298
|
|
|
380
|
-
// Step
|
|
299
|
+
// Step 8 — Recalculate every connection line endpoint.
|
|
381
300
|
svg.querySelectorAll('[data-conn-from][data-conn-to]').forEach(line => {
|
|
382
301
|
const fId = line.getAttribute('data-conn-from');
|
|
383
302
|
const tId = line.getAttribute('data-conn-to');
|
|
@@ -385,23 +304,23 @@ function fixLayout(layout) {
|
|
|
385
304
|
const fAc = layout.actors.find(a => a.id === fId);
|
|
386
305
|
const tUC = layout.useCases.find(u => u.id === tId);
|
|
387
306
|
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;
|
|
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;
|
|
390
309
|
if (!from || !to) return;
|
|
391
310
|
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);
|
|
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);
|
|
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);
|
|
396
315
|
});
|
|
397
316
|
|
|
398
|
-
// Step
|
|
317
|
+
// Step 9 — Resize boundary rect and SVG viewBox.
|
|
399
318
|
const br = svg.querySelector('.system-boundary, rect[rx="12"]');
|
|
400
|
-
if (br) br.setAttribute('height',
|
|
319
|
+
if (br) br.setAttribute('height', b.height);
|
|
401
320
|
const vb = svg.getAttribute('viewBox');
|
|
402
321
|
if (vb) {
|
|
403
322
|
const p = vb.split(/\s+/);
|
|
404
|
-
p[3] = Math.max(parseFloat(p[3]),
|
|
323
|
+
p[3] = Math.max(parseFloat(p[3]), b.y + b.height + 120);
|
|
405
324
|
svg.setAttribute('viewBox', p.join(' '));
|
|
406
325
|
}
|
|
407
326
|
}
|
|
@@ -424,9 +343,39 @@ function fixLayout(layout) {
|
|
|
424
343
|
|
|
425
344
|
---
|
|
426
345
|
|
|
427
|
-
##
|
|
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)
|
|
428
377
|
|
|
429
|
-
Every UC ellipse **and every actor
|
|
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.
|
|
430
379
|
|
|
431
380
|
```javascript
|
|
432
381
|
const useCaseDescriptions = {
|
|
@@ -469,27 +418,26 @@ document.addEventListener('click', e => {
|
|
|
469
418
|
```
|
|
470
419
|
|
|
471
420
|
UC group: `<g id="uc-uc1" data-uc-id="uc1" onclick="showDesc('uc1','uc',event)" style="cursor:pointer">`
|
|
472
|
-
|
|
473
421
|
Actor group: `<g id="actor-student" data-actor-id="student" onclick="showDesc('student','actor',event)" style="cursor:pointer">`
|
|
474
422
|
|
|
475
|
-
**Association
|
|
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.
|
|
476
424
|
|
|
477
425
|
---
|
|
478
426
|
|
|
479
|
-
##
|
|
427
|
+
## 9. diagramModel (mandatory)
|
|
480
428
|
|
|
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
|
|
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.
|
|
482
430
|
|
|
483
431
|
---
|
|
484
432
|
|
|
485
|
-
##
|
|
433
|
+
## 10. Styling
|
|
486
434
|
|
|
487
435
|
```
|
|
488
436
|
Background: #f8fafc, dot-grid (24px, #cbd5e1, r=0.8)
|
|
489
437
|
System boundary: fill white, stroke #3b82f6 (2.5px), header fill #1e40af, rx=12
|
|
490
438
|
Ellipse: fill #d4e9f7, stroke #2980b9 (1.8px), solid
|
|
491
439
|
Ellipse text: #1a1a2e, 11px, weight 500, text-anchor middle
|
|
492
|
-
Actor stick figure: stroke #334155 (2px), head fill #e0f0ff
|
|
440
|
+
Actor stick figure: stroke #334155 (2px), human head fill #e0f0ff, system head fill #fef3c7
|
|
493
441
|
Actor label: #1e293b, 11px, weight 600, text-anchor middle
|
|
494
442
|
Association line: #334155, 1.8px, no arrowhead
|
|
495
443
|
Include/extend: #7c3aed, 1.6px, dasharray 6 4, open arrowhead, label 9.5px
|
|
@@ -500,7 +448,7 @@ Fonts: 'Noto Sans Hebrew' + 'IBM Plex Mono' from Google Fonts
|
|
|
500
448
|
|
|
501
449
|
---
|
|
502
450
|
|
|
503
|
-
##
|
|
451
|
+
## 11. HTML File Structure
|
|
504
452
|
|
|
505
453
|
```html
|
|
506
454
|
<!DOCTYPE html>
|
|
@@ -513,86 +461,84 @@ Fonts: 'Noto Sans Hebrew' + 'IBM Plex Mono' from Google Fonts
|
|
|
513
461
|
</head>
|
|
514
462
|
<body>
|
|
515
463
|
<!-- Tab bar (only when split) -->
|
|
516
|
-
<div class="tab-bar"
|
|
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 > 8 threshold</div>
|
|
517
471
|
|
|
518
472
|
<!-- Fix Layout button (above each diagram) -->
|
|
519
473
|
<div style="text-align:center;margin:8px 0;">
|
|
520
|
-
<button onclick="fixLayout(
|
|
474
|
+
<button onclick="fixLayout(currentLayout())" class="fix-btn">Fix Layout</button>
|
|
521
475
|
</div>
|
|
522
476
|
|
|
523
|
-
<!--
|
|
524
|
-
<div class="
|
|
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">
|
|
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">
|
|
529
480
|
<!-- dot grid, boundary, association lines, include/extend lines,
|
|
530
481
|
generalization lines, UC ellipse groups, actor groups -->
|
|
531
482
|
</svg>
|
|
532
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>
|
|
533
487
|
|
|
534
488
|
<!-- VP export button -->
|
|
535
489
|
<button class="vp-export-btn" onclick="exportXMI()">Download .xmi (Visual Paradigm)</button>
|
|
536
490
|
|
|
537
491
|
<script>
|
|
538
|
-
const
|
|
539
|
-
const useCaseDescriptions = {
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
function
|
|
545
|
-
function
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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
|
+
});
|
|
550
515
|
</script>
|
|
551
516
|
</body>
|
|
552
517
|
</html>
|
|
553
518
|
```
|
|
554
519
|
|
|
555
|
-
For
|
|
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.
|
|
556
521
|
|
|
557
522
|
---
|
|
558
523
|
|
|
559
|
-
##
|
|
560
|
-
|
|
561
|
-
- Save as `[system_name]_use_case_diagram.html`
|
|
562
|
-
- Briefly state: actor count, use case count, include/extend relationships, whether split (and why)
|
|
563
|
-
|
|
564
|
-
---
|
|
524
|
+
## 12. Output
|
|
565
525
|
|
|
566
|
-
|
|
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()`
|
|
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).
|
|
588
528
|
|
|
589
529
|
---
|
|
590
530
|
|
|
591
|
-
##
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
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()`.
|