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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sad-mcp",
3
- "version": "2.2.7",
3
+ "version": "2.2.8",
4
4
  "description": "MCP server for Software Analysis and Design course materials at BGU",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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 with an embedded SVG class diagram.
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: two classes know of each other |
67
- | Directed association | Solid | Open arrow | When navigation is one-way only |
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 | One class uses another transiently |
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
- When a many-to-many relationship needs to store data (e.g., enrollment has a grade), convert it to a mediating class:
81
- - Draw a regular class with a meaningful name
82
- - Connect it with two associations (each 1..* or 1 depending on the domain)
83
- - Add the data attributes to this class
84
- - This is different from an association class a mediating class is a full entity in the model
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
- ### Association class (מחלקת קשר)
87
- When a relationship itself has attributes but no independent identity:
88
- - Draw a regular class box with `«association class»` stereotype
89
- - Connect it to the **midpoint** of the association line with a **dashed line**, perpendicular where possible
90
- - Never connect it as a plain class with a dependency arrow — that is wrong UML
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" dir="rtl">
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>/* tabs, layout, export button, dot-grid */</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 (only when split) -->
201
- <div class="tab-bar">...</div>
202
-
203
- <!-- One SVG per part, shown/hidden by tab selection -->
204
- <div class="diagram-part" id="part-1">
205
- <div class="split-rationale">Split by package: ...</div>
206
- <svg>
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
- const diagramModel = { ... }; // model-shape.md for kind:'class'
216
- function xmiBody(elements, rels) { ... } // from export-buttons.md
217
- function exportXMI() { ... } // from export-buttons.md
218
- // xmlEscape, downloadFile helpers // from export-buttons.md
219
- // Tab switching
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, '&amp;').replace(/</g, '&lt;')
324
+ .replace(/>/g, '&gt;').replace(/"/g, '&quot;');
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 (not as a standalone class)
255
- - [ ] Many-to-many with data mediating class (not association class)
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