sad-mcp 2.2.6 → 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.
@@ -17,6 +17,26 @@ const SEND_ONLY_PATTERNS = [
17
17
  /^התראה\s/,
18
18
  /^הפצת\s/,
19
19
  ];
20
+ // Forbidden UC archetypes — always wrong regardless of system domain.
21
+ // Each entry carries the human-readable fix hint surfaced in the error message.
22
+ const FORBIDDEN_UCS = [
23
+ {
24
+ rx: /^(login|log[\s-]?in|sign[\s-]?in|logon|log[\s-]?on|authenticate|authentication)\b/i,
25
+ hint: "Authentication is a precondition, not a UC. Add as a precondition on every UC that requires it.",
26
+ },
27
+ {
28
+ rx: /^(logout|log[\s-]?out|sign[\s-]?out|logoff|log[\s-]?off)\b/i,
29
+ hint: "Logout is a session-management detail, not a user goal. Omit it.",
30
+ },
31
+ {
32
+ rx: /^(התחברות|כניסה\s+ל|כניסה\s+אל|אימות\s+משתמש)/i,
33
+ hint: "התחברות היא תנאי מוקדם, לא מקרה שימוש. הוסף כ-precondition לכל מקרה שימוש הדורש זאת.",
34
+ },
35
+ {
36
+ rx: /^(יציאה\s+מ|יציאה\s+מה)/i,
37
+ hint: "יציאה מהמערכת אינה מקרה שימוש — השמט.",
38
+ },
39
+ ];
20
40
  // Physical verbs that describe actions outside the IS. Allowed only when
21
41
  // wrapped by a system verb (רישום / הזנה / עדכון / סימון / Record / Enter / ...).
22
42
  const PHYSICAL_VERBS = [
@@ -106,6 +126,10 @@ export function validateModel(model) {
106
126
  if (!uc.name)
107
127
  err("uc-missing-name", path, "useCase is missing a name.");
108
128
  if (uc.name && typeof uc.name === "string") {
129
+ const forbidden = FORBIDDEN_UCS.find(({ rx }) => rx.test(uc.name));
130
+ if (forbidden) {
131
+ err("forbidden-uc", `${path}.name`, `UC "${uc.name}" is a forbidden archetype. ${forbidden.hint}`);
132
+ }
109
133
  if (looksSendOnly(uc.name)) {
110
134
  err("send-only-uc", `${path}.name`, `UC name "${uc.name}" looks send-only (CR #10). Fold into the UC that decided to send.`);
111
135
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sad-mcp",
3
- "version": "2.2.6",
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
@@ -32,6 +32,11 @@ The shared files prepended to this prompt define the layout recipes, model shape
32
32
  9. **Every system-time / cron actor association MUST carry a timing label** (e.g., "פעם ביום", "כל 5 דקות"). See §2 and §8 for rendering.
33
33
  10. **No "send-only" use cases.** If a UC's entire behavior is sending a message / notification / alert / email / SMS, it is not a UC — it is a step inside the UC that *decided* to send it. See §3.
34
34
  11. **All narrative UC documentation must be in Hebrew.** Description, preconditions, postconditions, every scenario flow-step text and extension text, user stories (`role` / `want` / `so` fields), assumptions, and open questions must all be written in Hebrew. User stories are rendered as "בתור [role], אני רוצה [want], כדי ש[so]" — write the field values in Hebrew accordingly. Identifier strings (`id`, `primaryActor: 'customer'`) stay ASCII. The validator (§12) rejects narrative fields that contain zero Hebrew characters.
35
+ 12. **Forbidden UC archetypes — never draw these, ever:**
36
+ - **Login / authentication** (`Login`, `Sign in`, `Authenticate`, `התחברות`, `כניסה למערכת`, `אימות משתמש`): Authentication is a precondition, not a user goal. Add it as a precondition on every UC that requires it; do not model it as a UC.
37
+ - **Logout** (`Logout`, `Sign out`, `יציאה מהמערכת`): Session termination is a system-infrastructure detail, not a user goal. Omit entirely.
38
+ - **Send mail / send notification** — already covered by CR #10, repeated here for emphasis. `שליחת דוא"ל`, `שליחת SMS`, `שליחת התראה`, `Send email`, `Send notification` are never UCs. These are steps inside the UC that decided to send.
39
+ The validator (§12) raises an ERROR on any of the above.
35
40
 
36
41
  ---
37
42
 
@@ -80,7 +85,7 @@ The shared files prepended to this prompt define the layout recipes, model shape
80
85
 
81
86
  **Implied UCs**: every entity the system references needs at least one UC that manages it.
82
87
 
83
- **No "send-only" use cases.** A UC must carry non-trivial behavior (decision, data entry, state change, non-trivial computation). If the only behavior is firing a message, it is not a UC — it is a system step inside the UC that *decided* to send.
88
+ **No login, logout, or send-only use cases.** Login and logout are infrastructure details — never model them as UCs; add authentication as a precondition where needed. A UC must also carry non-trivial behavior (decision, data entry, state change, non-trivial computation). If the only behavior is firing a message, it is not a UC — it is a system step inside the UC that *decided* to send.
84
89
 
85
90
  - ❌ Wrong UCs: `שליחת התראה`, `שליחת מייל אישור`, `שליחת SMS`, `הודעה למנהל`, `שליחת דוח`.
86
91
  - ✅ Fold into the decider: the alert send-step belongs inside `עדכון רמות מלאי` (which *detected* the low stock); the confirmation email belongs inside `אישור הזמנה` (which *decided* to confirm).
@@ -889,6 +894,7 @@ uml_usecase_validate_model(model_json = JSON.stringify(diagramModel))
889
894
  The validator runs locally (no API cost) and catches what the §4 and CR gates cannot enforce from prose:
890
895
  - Include target with an actor association (CR #4) → ERROR
891
896
  - Single-incoming «include» (CR #4) → ERROR
897
+ - Forbidden UC archetypes: login/authenticate, logout, send-mail/notification (CR #10, CR #12) → ERROR
892
898
  - Send-only UC names like `Print Shipping Label` / `שליחת X` (CR #10) → ERROR
893
899
  - Physical-verb UC names (`Perform X`, `ביצוע X`) not wrapped with a system verb (CR #8) → WARNING
894
900
  - Primary actor missing, unknown, or not connected as initiator (§4.1/§4.4) → ERROR