sad-mcp 2.2.7 → 2.2.8
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-class-diagram/SKILL.md +269 -40
package/package.json
CHANGED
|
@@ -14,7 +14,8 @@ description: >
|
|
|
14
14
|
|
|
15
15
|
> **Canonical authority:** The notation rules in `NOTATION.md` (sibling file) are the ground truth. If any rule below conflicts with `NOTATION.md`, `NOTATION.md` wins.
|
|
16
16
|
|
|
17
|
-
Produce a single self-contained interactive HTML file
|
|
17
|
+
Produce a single self-contained interactive HTML file. The SVG is **built at runtime by inline JavaScript**: Claude generates `CLASSES` and `RELS` data arrays; a `render()` engine (§9d) builds the SVG from them. This is essential — SVG cannot measure text at generation time, so box heights must be computed in JS from data, not hardcoded by Claude.
|
|
18
|
+
|
|
18
19
|
The shared files prepended to this prompt define the layout recipes, model shape, and export button you must use.
|
|
19
20
|
|
|
20
21
|
---
|
|
@@ -44,6 +45,20 @@ Before drawing, extract:
|
|
|
44
45
|
- Visibility: `+` public, `-` private, `#` protected
|
|
45
46
|
- Stereotype: when present, render above the class name in the header at 9px; increase header height to ≥38px so both lines are readable
|
|
46
47
|
|
|
48
|
+
### What NOT to model as a class
|
|
49
|
+
|
|
50
|
+
Before adding any class, verify: **"Does the system store and manage records of this entity?"**
|
|
51
|
+
- **Yes** → model as a class.
|
|
52
|
+
- **No** → do not add it.
|
|
53
|
+
|
|
54
|
+
Two common false positives:
|
|
55
|
+
|
|
56
|
+
**1. Computed outputs** — reports, invoices-as-output, receipts, confirmation emails, search results. These are system behavior (use cases), not persistent entities. Do not add them as classes.
|
|
57
|
+
|
|
58
|
+
**2. Real-world participants the system never tracks** — if an entity is mentioned only as a real-world actor ("the customer calls", "the supplier sends goods") but the system never stores their record, do not model them as a class.
|
|
59
|
+
|
|
60
|
+
**UC diagram cross-check:** if a candidate class has no associations to any use case in the UC diagram, it almost certainly does not belong in the domain model. Verify before adding it.
|
|
61
|
+
|
|
47
62
|
### Enumerations
|
|
48
63
|
- Stereotype header: `«enumeration»`
|
|
49
64
|
- Styling: gray dashed border (`#94a3b8`, dasharray 5 3), gray header (`#475569`), `#f8fafc` fill
|
|
@@ -63,12 +78,17 @@ Before drawing, extract:
|
|
|
63
78
|
|
|
64
79
|
| Relationship | Line | Arrowhead | When to use |
|
|
65
80
|
|---|---|---|---|
|
|
66
|
-
| Association | Solid | None | Default
|
|
67
|
-
| Directed association | Solid | Open arrow |
|
|
81
|
+
| Association | Solid | **None** | Default for all class-to-class relationships in a domain/analysis diagram — plain lines only |
|
|
82
|
+
| Directed association | Solid | Open arrow | **NEVER in domain/analysis diagrams.** Only in design-level diagrams when documenting one-way navigation in code |
|
|
68
83
|
| Aggregation | Solid | Hollow diamond at whole | "has-a", part can exist independently |
|
|
69
84
|
| Composition | Solid | Filled diamond at whole | Part cannot exist without whole |
|
|
70
85
|
| Generalization | Solid | Hollow triangle at parent | True is-a; subtypes have different attributes/ops |
|
|
71
|
-
| Dependency | Dashed | Open arrow |
|
|
86
|
+
| Dependency | Dashed | Open arrow | **ONLY** when a class references an enumeration type as an attribute type. Never between two regular classes — use plain association instead |
|
|
87
|
+
|
|
88
|
+
> **CRITICAL — domain diagram association rules:**
|
|
89
|
+
> - Plain association (──): the default for every class-to-class relationship. No arrowheads.
|
|
90
|
+
> - Dependency (---→): only for enumeration usage. Never on class-to-class relationships.
|
|
91
|
+
> - Directed association (──→): never in a domain or analysis diagram.
|
|
72
92
|
|
|
73
93
|
**Multiplicity at both ends of every association**: `1`, `0..1`, `1..*`, `0..*`. Never leave one end unlabeled.
|
|
74
94
|
|
|
@@ -76,18 +96,25 @@ Before drawing, extract:
|
|
|
76
96
|
|
|
77
97
|
**Constraint notation**: if a business rule constrains an attribute or association, add `{constraint text}` near the relevant end.
|
|
78
98
|
|
|
79
|
-
### Mediating class
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
99
|
+
### Mediating class vs Association class — never confuse these two
|
|
100
|
+
|
|
101
|
+
| | Mediating class (מחלקה מתווכת) | Association class (מחלקת קשר) |
|
|
102
|
+
|---|---|---|
|
|
103
|
+
| **When** | Intermediate entity has independent identity and lifecycle | Relationship itself has attributes; no independent lifecycle |
|
|
104
|
+
| **Drawing** | Regular class box with two **separate** associations replacing the many-to-many | Regular class box connected to the **midpoint** of an existing association line via a **dashed perpendicular line** |
|
|
105
|
+
| **The original association** | Is **removed** — the mediating class replaces it | **Remains** — the association class is attached to it |
|
|
106
|
+
| **Example** | `OrderLine` between `Order` and `Product` | `Enrollment` (grade) between `Student` and `Course` |
|
|
85
107
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
|
|
108
|
+
**Mediating class (מחלקה מתווכת):**
|
|
109
|
+
- Replaces a many-to-many association entirely
|
|
110
|
+
- Drawn as a normal class with two separate associations pointing to each side
|
|
111
|
+
- Has its own primary key, independent existence
|
|
112
|
+
|
|
113
|
+
**Association class (מחלקת קשר):**
|
|
114
|
+
- Does NOT replace the association — the association line stays
|
|
115
|
+
- Connected to the **midpoint** of that line with a dashed perpendicular line
|
|
116
|
+
- Stereotype: `«association class»`
|
|
117
|
+
- Never connect it with a dependency arrow — that is wrong UML
|
|
91
118
|
|
|
92
119
|
### Generalization (inheritance)
|
|
93
120
|
Use only when subtypes have genuinely different attributes or operations:
|
|
@@ -114,6 +141,8 @@ MARGIN_LEFT = 60, MARGIN_TOP = 80
|
|
|
114
141
|
```
|
|
115
142
|
Enumerations share the grid with the classes that reference them (not in a separate zone).
|
|
116
143
|
|
|
144
|
+
The resulting `x, y` values for each class go directly into the `CLASSES` data array (§9b). Use `boxW ≈ max(120, longestString * 6.5 + 16)` to estimate widths for routing; the runtime recomputes actual widths from text content.
|
|
145
|
+
|
|
117
146
|
### Step 3 — Post-placement safety pass with `collision-nudge`
|
|
118
147
|
Run `collision-nudge` after step 2. After nudging, recompute all relationship endpoints using `rect-boundary-intersect`.
|
|
119
148
|
|
|
@@ -167,6 +196,12 @@ Hollow diamond: fill white, stroke #334155
|
|
|
167
196
|
Font: 'IBM Plex Mono', monospace (Google Fonts import)
|
|
168
197
|
```
|
|
169
198
|
|
|
199
|
+
### SVG rendering rules (enforced by §9d engine)
|
|
200
|
+
|
|
201
|
+
- `<html>` must **not** have `dir="rtl"` — SVG coordinates are always LTR.
|
|
202
|
+
- Class name: `text-anchor="middle"`. Attribute and operation rows: `text-anchor="start"` at `x = box.x + 6`. Never use `text-anchor="middle"` for attribute/op rows.
|
|
203
|
+
- Box heights are computed by `boxH()` in the §9d engine — never hardcode them.
|
|
204
|
+
|
|
170
205
|
---
|
|
171
206
|
|
|
172
207
|
## 7. Legend Bar
|
|
@@ -187,45 +222,232 @@ Define this JavaScript variable in every generated HTML `<script>` block, follow
|
|
|
187
222
|
|
|
188
223
|
## 9. HTML File Structure
|
|
189
224
|
|
|
225
|
+
The file uses **JS-driven rendering**: Claude fills in `CLASSES` + `RELS` data; the engine builds the SVG at page load. Do not write raw SVG with class boxes — use the pattern below.
|
|
226
|
+
|
|
227
|
+
### 9a. Page skeleton
|
|
228
|
+
|
|
190
229
|
```html
|
|
191
230
|
<!DOCTYPE html>
|
|
192
|
-
<html lang="he"
|
|
231
|
+
<html lang="he">
|
|
193
232
|
<head>
|
|
194
233
|
<meta charset="UTF-8">
|
|
195
234
|
<title>[Diagram title]</title>
|
|
196
235
|
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;700&display=swap" rel="stylesheet">
|
|
197
|
-
<style
|
|
236
|
+
<style>
|
|
237
|
+
body { margin: 0; background: #f1f5f9; font-family: 'IBM Plex Mono', monospace; }
|
|
238
|
+
.tab-bar { display: flex; gap: 4px; padding: 8px; background: #e2e8f0; }
|
|
239
|
+
.tab-btn { padding: 4px 12px; border: 1px solid #cbd5e1; background: white; cursor: pointer; border-radius: 4px; }
|
|
240
|
+
.tab-btn.active { background: #1e40af; color: white; }
|
|
241
|
+
.split-rationale { font-size: 11px; color: #64748b; padding: 4px 8px; }
|
|
242
|
+
svg { display: block; }
|
|
243
|
+
/* VP export button styles from export-buttons.md */
|
|
244
|
+
</style>
|
|
198
245
|
</head>
|
|
199
246
|
<body>
|
|
200
|
-
<!-- Tab bar
|
|
201
|
-
<div class="tab-bar"
|
|
202
|
-
|
|
203
|
-
<!--
|
|
204
|
-
<
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
<!-- dot-grid background, classes, enumerations, edges, legend -->
|
|
208
|
-
</svg>
|
|
209
|
-
</div>
|
|
210
|
-
|
|
211
|
-
<!-- Visual Paradigm export button (from export-buttons.md) -->
|
|
247
|
+
<!-- Tab bar — include only when split (§5) -->
|
|
248
|
+
<div class="tab-bar" id="tab-bar"></div>
|
|
249
|
+
|
|
250
|
+
<!-- SVG target; JS populates it -->
|
|
251
|
+
<svg id="diagram-svg" xmlns="http://www.w3.org/2000/svg"></svg>
|
|
252
|
+
|
|
253
|
+
<!-- VP export button (from export-buttons.md) -->
|
|
212
254
|
<button class="vp-export-btn" onclick="exportXMI()">Download .xmi (Visual Paradigm)</button>
|
|
213
255
|
|
|
214
256
|
<script>
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
function showPart(id) {
|
|
221
|
-
document.querySelectorAll('.diagram-part').forEach(p => p.hidden = true);
|
|
222
|
-
document.getElementById(id).hidden = false;
|
|
223
|
-
}
|
|
257
|
+
/* 1. DATA — Claude fills in CLASSES and RELS (§9b, §9c) */
|
|
258
|
+
/* 2. ENGINE — copy §9d verbatim */
|
|
259
|
+
/* 3. VP EXPORT — from export-buttons.md */
|
|
260
|
+
/* 4. diagramModel — from model-shape.md for kind:'class'*/
|
|
261
|
+
document.addEventListener('DOMContentLoaded', render);
|
|
224
262
|
</script>
|
|
225
263
|
</body>
|
|
226
264
|
</html>
|
|
227
265
|
```
|
|
228
266
|
|
|
267
|
+
### 9b. CLASSES data (Claude generates this)
|
|
268
|
+
|
|
269
|
+
```javascript
|
|
270
|
+
const CLASSES = [
|
|
271
|
+
{
|
|
272
|
+
id: 'Order', // unique key — referenced by RELS
|
|
273
|
+
name: 'Order', // display name
|
|
274
|
+
stereotype: null, // null | '«enumeration»' | '«abstract»' | custom string
|
|
275
|
+
isEnum: false, // true → enum styling (gray dashed border + header)
|
|
276
|
+
isAbstract: false, // true → name in italic
|
|
277
|
+
attrs: [ // attribute strings, as they appear in the box
|
|
278
|
+
'+ orderId : String',
|
|
279
|
+
'+ date : Date',
|
|
280
|
+
'+ status : OrderStatus'
|
|
281
|
+
],
|
|
282
|
+
ops: [ // operation strings — rendered italic
|
|
283
|
+
'+ calculateTotal() : Money'
|
|
284
|
+
],
|
|
285
|
+
x: 60, // top-left x from layout algorithm (§4)
|
|
286
|
+
y: 80 // top-left y from layout algorithm (§4)
|
|
287
|
+
}
|
|
288
|
+
// ... more entries
|
|
289
|
+
];
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 9c. RELS data (Claude generates this)
|
|
293
|
+
|
|
294
|
+
```javascript
|
|
295
|
+
const RELS = [
|
|
296
|
+
{
|
|
297
|
+
type: 'assoc', // 'assoc' | 'agg' | 'comp' | 'gen' | 'dep'
|
|
298
|
+
from: 'Order', // class id
|
|
299
|
+
to: 'Customer',
|
|
300
|
+
fromMult: '0..*', // null for generalization
|
|
301
|
+
toMult: '1',
|
|
302
|
+
fromRole: null, // null or role name string
|
|
303
|
+
toRole: null,
|
|
304
|
+
label: null, // optional mid-line label
|
|
305
|
+
points: [ // orthogonal polyline — Claude computes from layout positions
|
|
306
|
+
[200, 130], [200, 200], [340, 200]
|
|
307
|
+
]
|
|
308
|
+
}
|
|
309
|
+
];
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**Computing `points`:** estimate `boxW ≈ max(120, longestString * 6.5 + 16)` for routing. All consecutive pairs must share x or y (orthogonal). Start/end at the nearest box edge.
|
|
313
|
+
|
|
314
|
+
### 9d. Rendering engine (copy verbatim into `<script>`)
|
|
315
|
+
|
|
316
|
+
```javascript
|
|
317
|
+
// ── Constants ─────────────────────────────────────────────────────────────
|
|
318
|
+
const HDR_H = 28, PAD = 6, LINE_H = 14, DIV_H = 1;
|
|
319
|
+
const FM = 'font-family="IBM Plex Mono, monospace"';
|
|
320
|
+
|
|
321
|
+
function escXml(s) {
|
|
322
|
+
return String(s)
|
|
323
|
+
.replace(/&/g, '&').replace(/</g, '<')
|
|
324
|
+
.replace(/>/g, '>').replace(/"/g, '"');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function boxH(cls) {
|
|
328
|
+
const hdrH = cls.stereotype ? 38 : HDR_H;
|
|
329
|
+
return hdrH + PAD + cls.attrs.length * LINE_H +
|
|
330
|
+
(cls.ops.length > 0 ? DIV_H + cls.ops.length * LINE_H : 0) + PAD;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function boxW(cls) {
|
|
334
|
+
const longest = [...cls.attrs, ...cls.ops, cls.name]
|
|
335
|
+
.reduce((m, s) => Math.max(m, s.length), 0);
|
|
336
|
+
return Math.max(120, Math.ceil(longest * 6.5) + 16);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function svgBox(cls) {
|
|
340
|
+
const w = boxW(cls), h = boxH(cls), { x, y } = cls;
|
|
341
|
+
const hdrH = cls.stereotype ? 38 : HDR_H;
|
|
342
|
+
const border = cls.isEnum ? '#94a3b8' : '#3b82f6';
|
|
343
|
+
const hdrFill = cls.isEnum ? '#475569' : '#1e40af';
|
|
344
|
+
const bodyFill = cls.isEnum ? '#f8fafc' : 'white';
|
|
345
|
+
const dash = cls.isEnum ? 'stroke-dasharray="5 3"' : '';
|
|
346
|
+
|
|
347
|
+
let s = `<rect x="${x}" y="${y}" width="${w}" height="${h}" fill="${bodyFill}" stroke="${border}" stroke-width="2" ${dash} rx="2"/>`;
|
|
348
|
+
s += `<rect x="${x}" y="${y}" width="${w}" height="${hdrH}" fill="${hdrFill}" rx="2"/>`;
|
|
349
|
+
|
|
350
|
+
if (cls.stereotype) {
|
|
351
|
+
s += `<text x="${x + w/2}" y="${y + 13}" text-anchor="middle" fill="white" font-size="9" ${FM}>${escXml(cls.stereotype)}</text>`;
|
|
352
|
+
}
|
|
353
|
+
const nameStyle = cls.isAbstract ? 'font-style:italic' : '';
|
|
354
|
+
const nameY = cls.stereotype ? y + hdrH - 8 : y + hdrH / 2 + 4;
|
|
355
|
+
s += `<text x="${x + w/2}" y="${nameY}" text-anchor="middle" fill="white" font-size="11" font-weight="700" ${FM} style="${nameStyle}">${escXml(cls.name)}</text>`;
|
|
356
|
+
|
|
357
|
+
const divY = y + hdrH;
|
|
358
|
+
s += `<line x1="${x}" y1="${divY}" x2="${x + w}" y2="${divY}" stroke="${border}" stroke-width="1"/>`;
|
|
359
|
+
|
|
360
|
+
let curY = divY + PAD + 10;
|
|
361
|
+
for (const a of cls.attrs) {
|
|
362
|
+
s += `<text x="${x + 6}" y="${curY}" text-anchor="start" fill="#1e293b" font-size="10.5" ${FM}>${escXml(a)}</text>`;
|
|
363
|
+
curY += LINE_H;
|
|
364
|
+
}
|
|
365
|
+
if (cls.ops.length > 0) {
|
|
366
|
+
const opDivY = divY + PAD + cls.attrs.length * LINE_H;
|
|
367
|
+
s += `<line x1="${x}" y1="${opDivY}" x2="${x + w}" y2="${opDivY}" stroke="${border}" stroke-width="1"/>`;
|
|
368
|
+
curY = opDivY + PAD + 10;
|
|
369
|
+
for (const o of cls.ops) {
|
|
370
|
+
s += `<text x="${x + 6}" y="${curY}" text-anchor="start" fill="#2563eb" font-size="10.5" font-style="italic" ${FM}>${escXml(o)}</text>`;
|
|
371
|
+
curY += LINE_H;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return s;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function svgRel(rel) {
|
|
378
|
+
const pts = rel.points.map(([px, py]) => `${px},${py}`).join(' ');
|
|
379
|
+
let mS = '', mE = '', dash = '';
|
|
380
|
+
if (rel.type === 'comp') mS = 'marker-start="url(#compFill)"';
|
|
381
|
+
if (rel.type === 'agg') mS = 'marker-start="url(#aggHollow)"';
|
|
382
|
+
if (rel.type === 'gen') mE = 'marker-end="url(#genHollow)"';
|
|
383
|
+
if (rel.type === 'dep') { mE = 'marker-end="url(#arrOpen)"'; dash = 'stroke-dasharray="6 3"'; }
|
|
384
|
+
// 'assoc': no markers — plain line
|
|
385
|
+
|
|
386
|
+
let s = `<polyline points="${pts}" fill="none" stroke="#334155" stroke-width="1.8" ${dash} ${mS} ${mE}/>`;
|
|
387
|
+
if (rel.fromMult) {
|
|
388
|
+
const [fx, fy] = rel.points[0];
|
|
389
|
+
s += `<text x="${fx + 4}" y="${fy - 4}" fill="#64748b" font-size="9.5" ${FM}>${escXml(rel.fromMult)}</text>`;
|
|
390
|
+
}
|
|
391
|
+
if (rel.toMult) {
|
|
392
|
+
const [tx, ty] = rel.points[rel.points.length - 1];
|
|
393
|
+
s += `<text x="${tx + 4}" y="${ty - 4}" fill="#64748b" font-size="9.5" ${FM}>${escXml(rel.toMult)}</text>`;
|
|
394
|
+
}
|
|
395
|
+
if (rel.label) {
|
|
396
|
+
const mid = rel.points[Math.floor(rel.points.length / 2)];
|
|
397
|
+
s += `<text x="${mid[0]}" y="${mid[1] - 6}" text-anchor="middle" fill="#64748b" font-size="9.5" ${FM}>${escXml(rel.label)}</text>`;
|
|
398
|
+
}
|
|
399
|
+
return s;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function render() {
|
|
403
|
+
let maxX = 600, maxY = 400;
|
|
404
|
+
for (const cls of CLASSES) {
|
|
405
|
+
maxX = Math.max(maxX, cls.x + boxW(cls) + 60);
|
|
406
|
+
maxY = Math.max(maxY, cls.y + boxH(cls) + 80);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const defs = `<defs>
|
|
410
|
+
<pattern id="dotgrid" width="24" height="24" patternUnits="userSpaceOnUse">
|
|
411
|
+
<circle cx="12" cy="12" r="0.8" fill="#cbd5e1"/>
|
|
412
|
+
</pattern>
|
|
413
|
+
<marker id="genHollow" markerWidth="12" markerHeight="10" refX="10" refY="5" orient="auto">
|
|
414
|
+
<polygon points="0,0 10,5 0,10" fill="white" stroke="#334155" stroke-width="1.5"/>
|
|
415
|
+
</marker>
|
|
416
|
+
<marker id="arrOpen" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto">
|
|
417
|
+
<polyline points="0,0 9,4 0,8" fill="none" stroke="#334155" stroke-width="1.5"/>
|
|
418
|
+
</marker>
|
|
419
|
+
<marker id="compFill" markerWidth="14" markerHeight="10" refX="1" refY="5" orient="auto">
|
|
420
|
+
<polygon points="0,5 7,0 14,5 7,10" fill="#1e40af" stroke="#1e40af"/>
|
|
421
|
+
</marker>
|
|
422
|
+
<marker id="aggHollow" markerWidth="14" markerHeight="10" refX="1" refY="5" orient="auto">
|
|
423
|
+
<polygon points="0,5 7,0 14,5 7,10" fill="white" stroke="#334155"/>
|
|
424
|
+
</marker>
|
|
425
|
+
</defs>`;
|
|
426
|
+
|
|
427
|
+
// totalWidth / totalHeight include margins and space for the legend.
|
|
428
|
+
// RIGHT_MARGIN ≥ 40px, BOTTOM_MARGIN ≥ 70px (legend needs ~50px).
|
|
429
|
+
// These are already baked into maxX / maxY from the loop above (60px / 80px).
|
|
430
|
+
const totalWidth = maxX;
|
|
431
|
+
const totalHeight = maxY; // legend drawn at totalHeight - 60, inside this height
|
|
432
|
+
|
|
433
|
+
const bg = `<rect width="${totalWidth}" height="${totalHeight}" fill="url(#dotgrid)"/>`;
|
|
434
|
+
const rels = RELS.map(svgRel).join('');
|
|
435
|
+
const boxes = CLASSES.map(svgBox).join('');
|
|
436
|
+
// Legend is placed at totalHeight - 60 so it always lands inside the viewBox.
|
|
437
|
+
const legend = `<text x="16" y="${totalHeight - 60}" fill="#64748b" font-size="9" ${FM}>── Association ◇── Aggregation ◆── Composition ──▷ Generalization --→ Dependency (enum only)</text>`;
|
|
438
|
+
|
|
439
|
+
const svg = document.getElementById('diagram-svg');
|
|
440
|
+
svg.setAttribute('width', totalWidth);
|
|
441
|
+
svg.setAttribute('height', totalHeight);
|
|
442
|
+
svg.setAttribute('viewBox', `0 0 ${totalWidth} ${totalHeight}`);
|
|
443
|
+
svg.innerHTML = defs + bg + rels + boxes + legend;
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
### 9e. Multi-part (tabbed) diagrams
|
|
448
|
+
|
|
449
|
+
When splitting (§5), generate one `<svg id="svg-part-N">` per part. Split `CLASSES` and `RELS` into per-part arrays. Call `render()` variants that target each SVG element. Tab buttons toggle `hidden` and call the matching render function.
|
|
450
|
+
|
|
229
451
|
---
|
|
230
452
|
|
|
231
453
|
## 10. Output
|
|
@@ -248,14 +470,21 @@ When in doubt, use the male form.
|
|
|
248
470
|
|
|
249
471
|
## 12. Common Mistakes — Check Before Delivering
|
|
250
472
|
|
|
473
|
+
- [ ] Every class passes the persistence test: "Does the system store and manage records of this entity?" — computed outputs (reports, receipts) and real-world participants the system never tracks are not domain entities
|
|
251
474
|
- [ ] Every association has multiplicity at **both** ends
|
|
252
475
|
- [ ] All lines are orthogonal — run the x1===x2 || y1===y2 check on every polyline
|
|
253
476
|
- [ ] No line segment passes through any class bounding box — use `rect-boundary-intersect` for all endpoints
|
|
254
|
-
- [ ] Association class connected to midpoint of association via dashed line
|
|
255
|
-
- [ ]
|
|
477
|
+
- [ ] Association class (מחלקת קשר): connected to the **midpoint** of the original association line via a dashed perpendicular line; the original association remains
|
|
478
|
+
- [ ] Mediating class (מחלקה מתווכת): **replaces** the many-to-many with two separate associations; drawn as a plain class, NOT connected to a line midpoint
|
|
256
479
|
- [ ] Enumerations placed near using class (not in a fixed zone)
|
|
257
480
|
- [ ] diagramModel defined in `<script>` and matches model-shape.md
|
|
258
481
|
- [ ] VP export button present and wired to `exportXMI()`
|
|
259
482
|
- [ ] If split: each part has a visible split-rationale line
|
|
260
483
|
- [ ] Operations text is italic
|
|
261
484
|
- [ ] Hebrew names use male form
|
|
485
|
+
- [ ] **No raw SVG class boxes** — class boxes are rendered by `svgBox()` from `CLASSES` data, not hand-written SVG
|
|
486
|
+
- [ ] **No arrowheads on plain associations** — `type: 'assoc'` in RELS produces no marker; directed association never appears in domain/analysis diagrams
|
|
487
|
+
- [ ] **Dependency (---→) only for enum usage** — `type: 'dep'` only when a class references an enum type; never between two regular classes
|
|
488
|
+
- [ ] **`<html>` has no `dir="rtl"`** — SVG coordinates are LTR; page language does not affect SVG rendering
|
|
489
|
+
- [ ] **`boxH()` / `boxW()` from §9d engine used** — never hardcode box dimensions in data or SVG
|
|
490
|
+
- [ ] **Attribute/operation rows: `text-anchor="start"` at `x = box.x + 6`** — enforced by engine; do not override
|