sad-mcp 2.0.0 → 2.1.1

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/dist/tools.js CHANGED
@@ -8,6 +8,7 @@ import { extractText, isExtractable } from "./text-extract.js";
8
8
  import { loadTextCache, saveTextEntry } from "./text-cache.js";
9
9
  import { trackToolCall } from "./tracking.js";
10
10
  import { renderBPMN, parseAndValidate, formatIssues, } from "./bpmn/index.js";
11
+ import { parseAndValidate as ucParseAndValidate, formatIssues as ucFormatIssues, } from "./usecase/index.js";
11
12
  const __tools_filename = fileURLToPath(import.meta.url);
12
13
  const __tools_dirname = dirname(__tools_filename);
13
14
  const PKG = JSON.parse(readFileSync(join(__tools_dirname, "..", "package.json"), "utf-8"));
@@ -363,6 +364,20 @@ export function registerToolHandlers(server) {
363
364
  properties: {},
364
365
  },
365
366
  },
367
+ {
368
+ name: "uml_usecase_validate_model",
369
+ description: "Validate a use-case diagramModel JSON BEFORE generating the HTML file. Catches the §4/§4.4/CR violations the prompt cannot fully enforce: include target with actor association (CR #4), single-incoming include (CR #4), send-only UC names (CR #10), physical-verb names (CR #8), missing primary actor (§4.4), broken references in scenario flow steps, orphaned UCs, and unsafe wireframe HTML. Returns either 'Model valid' (optionally with warnings) or a list of specific errors to fix. Call iteratively — fix errors, call again — until valid. This check runs locally; no API cost.",
370
+ inputSchema: {
371
+ type: "object",
372
+ properties: {
373
+ model_json: {
374
+ type: "string",
375
+ description: "The diagramModel JSON as a string (the same object assigned to `diagramModel` in the generated HTML). See skills/_shared/model-shape.md for the required shape.",
376
+ },
377
+ },
378
+ required: ["model_json"],
379
+ },
380
+ },
366
381
  ],
367
382
  }));
368
383
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
@@ -689,6 +704,27 @@ export function registerToolHandlers(server) {
689
704
  trackToolCall(name, toolArgs, { success: true, responseChars: text.length }, Date.now() - startTime);
690
705
  return { content: [{ type: "text", text }] };
691
706
  }
707
+ if (name === "uml_usecase_validate_model") {
708
+ const modelJson = args?.model_json;
709
+ if (typeof modelJson !== "string" || modelJson.trim() === "") {
710
+ const text = "Error: model_json is required and must be a JSON string.";
711
+ trackToolCall(name, toolArgs, { success: false, responseChars: text.length }, Date.now() - startTime);
712
+ return { content: [{ type: "text", text }] };
713
+ }
714
+ const vr = ucParseAndValidate(modelJson);
715
+ if (vr.ok) {
716
+ const warnPart = vr.warnings.length > 0
717
+ ? `\n\nWarnings (non-blocking):\n${ucFormatIssues(vr.warnings)}`
718
+ : "";
719
+ const text = `✓ Model valid.${warnPart}\n\nYou may proceed to generate the HTML file (see skills/uml-use-case-diagram/SKILL.md §11).`;
720
+ trackToolCall(name, toolArgs, { success: true, responseChars: text.length }, Date.now() - startTime);
721
+ return { content: [{ type: "text", text }] };
722
+ }
723
+ const allWarnings = vr.warnings.length > 0 ? `\n\nWarnings (non-blocking):\n${ucFormatIssues(vr.warnings)}` : "";
724
+ const text = `✗ Model invalid — fix the following errors and call uml_usecase_validate_model again:\n\n${ucFormatIssues(vr.issues)}${allWarnings}`;
725
+ trackToolCall(name, toolArgs, { success: false, responseChars: text.length }, Date.now() - startTime);
726
+ return { content: [{ type: "text", text }] };
727
+ }
692
728
  throw new Error(`Unknown tool: ${name}`);
693
729
  });
694
730
  }
@@ -0,0 +1,2 @@
1
+ export { validateModel, parseAndValidate, formatIssues } from "./validate.js";
2
+ export type { ValidateResult, ValidationIssue } from "./validate.js";
@@ -0,0 +1 @@
1
+ export { validateModel, parseAndValidate, formatIssues } from "./validate.js";
@@ -0,0 +1,17 @@
1
+ export interface ValidationIssue {
2
+ severity: "error" | "warning";
3
+ code: string;
4
+ path: string;
5
+ message: string;
6
+ }
7
+ export type ValidateResult = {
8
+ ok: true;
9
+ warnings: ValidationIssue[];
10
+ } | {
11
+ ok: false;
12
+ issues: ValidationIssue[];
13
+ warnings: ValidationIssue[];
14
+ };
15
+ export declare function validateModel(model: unknown): ValidateResult;
16
+ export declare function parseAndValidate(jsonText: string): ValidateResult;
17
+ export declare function formatIssues(issues: ValidationIssue[]): string;
@@ -0,0 +1,393 @@
1
+ // Structural validator for UML use-case diagramModel JSON.
2
+ //
3
+ // Purpose: catch the business-rule violations the SKILL prompts for but cannot
4
+ // enforce on its own — overeager «include», send-only UCs, physical-verb
5
+ // labels, missing primary actor, broken references in scenario flows, etc.
6
+ //
7
+ // Returns ALL issues at once so the calling LLM can self-correct in a single
8
+ // pass rather than iterating error-by-error.
9
+ // ── Name heuristics (CR #8 and CR #10) ──────────────────────────────────
10
+ // Send-only / output-only verbs that on their own do not constitute a UC.
11
+ // Pattern allows "Record sending" etc. — only bare send-verbs flag.
12
+ const SEND_ONLY_PATTERNS = [
13
+ /^(send|notify|email|sms|alert|push|broadcast)\b/i,
14
+ /^(print)\s+\w+\s+(label|receipt|ticket|slip)\b/i,
15
+ /^שליחת\s/,
16
+ /^הודעה\s/,
17
+ /^התראה\s/,
18
+ /^הפצת\s/,
19
+ ];
20
+ // Physical verbs that describe actions outside the IS. Allowed only when
21
+ // wrapped by a system verb (רישום / הזנה / עדכון / סימון / Record / Enter / ...).
22
+ const PHYSICAL_VERBS = [
23
+ /^(perform|do|execute|conduct)\b/i,
24
+ /^(pick|pack|ship|deliver|receive|count|pay|collect)\b/i,
25
+ /^(ביצוע|הכנת|קבלת|מסירת|ספירת|תשלום|איסוף)\s/,
26
+ ];
27
+ const SYSTEM_VERB_WRAPPERS = [
28
+ /^(record|register|enter|log|capture|mark|update|create|save)\b/i,
29
+ /^(רישום|הזנת|עדכון|סימון|יצירת|שמירת|קליטת|תיעוד)\s/,
30
+ ];
31
+ function looksSendOnly(name) {
32
+ return SEND_ONLY_PATTERNS.some((rx) => rx.test(name.trim()));
33
+ }
34
+ function looksPhysical(name) {
35
+ const n = name.trim();
36
+ if (SYSTEM_VERB_WRAPPERS.some((rx) => rx.test(n)))
37
+ return false;
38
+ return PHYSICAL_VERBS.some((rx) => rx.test(n));
39
+ }
40
+ // CR #11 — narrative UC docs must be in Hebrew. We accept "contains at least
41
+ // one Hebrew character" as a cheap heuristic; trivial strings (empty, whitespace,
42
+ // pure punctuation) are treated as missing-content rather than English.
43
+ const HEBREW_RANGE = /[\u0590-\u05FF]/;
44
+ function hasHebrew(text) {
45
+ return typeof text === "string" && HEBREW_RANGE.test(text);
46
+ }
47
+ function isMeaningful(text) {
48
+ return typeof text === "string" && text.trim().length >= 2;
49
+ }
50
+ function asArray(v) {
51
+ return Array.isArray(v) ? v : [];
52
+ }
53
+ function getParts(model) {
54
+ if (!model || typeof model !== "object")
55
+ return [];
56
+ const parts = asArray(model.parts);
57
+ return parts.map((p) => {
58
+ const obj = (p && typeof p === "object") ? p : {};
59
+ return {
60
+ elements: asArray(obj.elements),
61
+ relationships: asArray(obj.relationships),
62
+ };
63
+ });
64
+ }
65
+ // ── Main validator ──────────────────────────────────────────────────────
66
+ export function validateModel(model) {
67
+ const errors = [];
68
+ const warnings = [];
69
+ const err = (code, path, message) => {
70
+ errors.push({ severity: "error", code, path, message });
71
+ };
72
+ const warn = (code, path, message) => {
73
+ warnings.push({ severity: "warning", code, path, message });
74
+ };
75
+ // --- Root shape ---
76
+ if (!model || typeof model !== "object") {
77
+ err("not-object", "(root)", "diagramModel must be a JSON object.");
78
+ return { ok: false, issues: errors, warnings };
79
+ }
80
+ const root = model;
81
+ if (root.kind !== "use-case") {
82
+ err("wrong-kind", "kind", `Expected kind: "use-case", got ${JSON.stringify(root.kind)}.`);
83
+ }
84
+ const parts = getParts(root);
85
+ if (parts.length === 0) {
86
+ err("no-parts", "parts", "parts[] is empty — at least one part (tab) is required.");
87
+ return { ok: false, issues: errors, warnings };
88
+ }
89
+ // Collect everything across all parts for cross-part id lookup.
90
+ const allElements = [];
91
+ const allRelationships = [];
92
+ parts.forEach((p, i) => {
93
+ p.elements.forEach((el) => allElements.push({ part: i, el }));
94
+ p.relationships.forEach((rel) => allRelationships.push({ part: i, rel }));
95
+ });
96
+ const ucs = allElements.filter((x) => x.el.kind === "useCase").map((x) => x.el);
97
+ const actors = allElements.filter((x) => x.el.kind === "actor").map((x) => x.el);
98
+ const ucIds = new Set(ucs.map((u) => String(u.id)));
99
+ const actorIds = new Set(actors.map((a) => String(a.id)));
100
+ // --- Element shape checks ---
101
+ ucs.forEach((uc) => {
102
+ const id = String(uc.id ?? "");
103
+ const path = `useCase[${id || "?"}]`;
104
+ if (!id)
105
+ err("uc-missing-id", path, "useCase is missing an id.");
106
+ if (!uc.name)
107
+ err("uc-missing-name", path, "useCase is missing a name.");
108
+ if (uc.name && typeof uc.name === "string") {
109
+ if (looksSendOnly(uc.name)) {
110
+ err("send-only-uc", `${path}.name`, `UC name "${uc.name}" looks send-only (CR #10). Fold into the UC that decided to send.`);
111
+ }
112
+ if (looksPhysical(uc.name)) {
113
+ warn("physical-verb", `${path}.name`, `UC name "${uc.name}" starts with a physical verb (CR #8). Prefer "Record X" / "Enter X" / "Register X" to describe the IS action.`);
114
+ }
115
+ }
116
+ });
117
+ actors.forEach((a) => {
118
+ const id = String(a.id ?? "");
119
+ const path = `actor[${id || "?"}]`;
120
+ if (!id)
121
+ err("actor-missing-id", path, "actor is missing an id.");
122
+ if (!a.name)
123
+ err("actor-missing-name", path, "actor is missing a name.");
124
+ });
125
+ // --- Relationship cross-references + CR #4 + single-include + orphaned UC ---
126
+ const actorAssocsByUc = new Map();
127
+ const includeIntoByUc = new Map();
128
+ const initiatorByUc = new Map();
129
+ allRelationships.forEach(({ rel }, i) => {
130
+ const path = `relationships[${i}]`;
131
+ const kind = rel.kind;
132
+ if (kind === "actorAssoc") {
133
+ const actorId = String(rel.actorId ?? "");
134
+ const ucId = String(rel.ucId ?? "");
135
+ if (!actorIds.has(actorId))
136
+ err("unknown-actor", path, `actorAssoc.actorId "${actorId}" does not exist in actors[].`);
137
+ if (!ucIds.has(ucId))
138
+ err("unknown-uc", path, `actorAssoc.ucId "${ucId}" does not exist in use cases.`);
139
+ if (actorIds.has(actorId) && ucIds.has(ucId)) {
140
+ if (!actorAssocsByUc.has(ucId))
141
+ actorAssocsByUc.set(ucId, []);
142
+ actorAssocsByUc.get(ucId).push(rel);
143
+ if (rel.role === "initiator") {
144
+ if (!initiatorByUc.has(ucId))
145
+ initiatorByUc.set(ucId, new Set());
146
+ initiatorByUc.get(ucId).add(actorId);
147
+ }
148
+ }
149
+ }
150
+ else if (kind === "include") {
151
+ const from = String(rel.from ?? "");
152
+ const to = String(rel.to ?? "");
153
+ if (!ucIds.has(from))
154
+ err("unknown-uc", path, `include.from "${from}" does not exist.`);
155
+ if (!ucIds.has(to))
156
+ err("unknown-uc", path, `include.to "${to}" does not exist.`);
157
+ if (ucIds.has(to)) {
158
+ if (!includeIntoByUc.has(to))
159
+ includeIntoByUc.set(to, []);
160
+ includeIntoByUc.get(to).push(rel);
161
+ }
162
+ }
163
+ else if (kind === "extend") {
164
+ const from = String(rel.from ?? "");
165
+ const to = String(rel.to ?? "");
166
+ if (!ucIds.has(from))
167
+ err("unknown-uc", path, `extend.from "${from}" does not exist.`);
168
+ if (!ucIds.has(to))
169
+ err("unknown-uc", path, `extend.to "${to}" does not exist.`);
170
+ }
171
+ else if (kind === "generalization") {
172
+ const from = String(rel.from ?? "");
173
+ const to = String(rel.to ?? "");
174
+ if (!ucIds.has(from) && !actorIds.has(from))
175
+ err("unknown-endpoint", path, `generalization.from "${from}" is neither a UC nor an actor.`);
176
+ if (!ucIds.has(to) && !actorIds.has(to))
177
+ err("unknown-endpoint", path, `generalization.to "${to}" is neither a UC nor an actor.`);
178
+ }
179
+ });
180
+ // CR #4 — include target checks
181
+ for (const [ucId, includes] of includeIntoByUc) {
182
+ const path = `useCase[${ucId}]`;
183
+ if (includes.length < 2) {
184
+ err("include-single-base", path, `UC "${ucId}" is the target of only ${includes.length} «include» arrow — an include target requires ≥2 bases (CR #4). Fold into the parent UC.`);
185
+ }
186
+ if ((actorAssocsByUc.get(ucId) || []).length > 0) {
187
+ err("include-target-has-actor", path, `UC "${ucId}" is an «include» target AND has a direct actor association — forbidden (CR #4). An include target must have ZERO actor associations.`);
188
+ }
189
+ }
190
+ // §4.1 — every UC must have ≥1 initiator (unless it's purely an include target)
191
+ ucs.forEach((uc) => {
192
+ const id = String(uc.id ?? "");
193
+ if (!id)
194
+ return;
195
+ const path = `useCase[${id}]`;
196
+ const isIncludeTarget = (includeIntoByUc.get(id) || []).length >= 2;
197
+ const hasInitiator = (initiatorByUc.get(id)?.size ?? 0) > 0;
198
+ const hasAnyActor = (actorAssocsByUc.get(id) || []).length > 0;
199
+ if (!isIncludeTarget && !hasAnyActor) {
200
+ err("uc-no-actor", path, `UC "${id}" has no actor association and is not an include target. Either connect an actor or remove the UC.`);
201
+ }
202
+ if (!isIncludeTarget && hasAnyActor && !hasInitiator) {
203
+ err("uc-no-initiator", path, `UC "${id}" has actor associations but none with role: "initiator" (§4.1). Every UC needs at least one initiator.`);
204
+ }
205
+ });
206
+ // §4.4 — per-UC documentation checks (only for non-include-target UCs; include
207
+ // targets are sub-behaviors and can use lighter docs in practice, but we still
208
+ // require description + at least one scenario if any doc is present).
209
+ ucs.forEach((uc) => {
210
+ const id = String(uc.id ?? "");
211
+ if (!id)
212
+ return;
213
+ const path = `useCase[${id}]`;
214
+ const isIncludeTarget = (includeIntoByUc.get(id) || []).length >= 2;
215
+ const primaryActor = uc.primaryActor;
216
+ if (!isIncludeTarget) {
217
+ if (!primaryActor) {
218
+ err("uc-missing-primary-actor", path, `UC "${id}" is missing primaryActor (§4.4).`);
219
+ }
220
+ else if (!actorIds.has(primaryActor)) {
221
+ err("uc-primary-actor-unknown", path, `UC "${id}" primaryActor "${primaryActor}" is not an actor.`);
222
+ }
223
+ else if (!(initiatorByUc.get(id)?.has(primaryActor))) {
224
+ err("uc-primary-actor-not-initiator", path, `UC "${id}" primaryActor "${primaryActor}" is not connected with role: "initiator". The primary actor must be an initiator.`);
225
+ }
226
+ }
227
+ const scenarios = asArray(uc.scenarios);
228
+ if (!isIncludeTarget && scenarios.length === 0) {
229
+ err("uc-no-scenarios", path, `UC "${id}" has no scenarios. At least one scenario with a flow is required (§4.4).`);
230
+ }
231
+ scenarios.forEach((sc, si) => {
232
+ const sco = (sc && typeof sc === "object") ? sc : {};
233
+ const scPath = `${path}.scenarios[${si}]`;
234
+ const flow = asArray(sco.flow);
235
+ if (flow.length === 0) {
236
+ err("scenario-no-flow", scPath, `Scenario has no flow steps. At least one step is required.`);
237
+ }
238
+ flow.forEach((step, stepIdx) => {
239
+ const s = (step && typeof step === "object") ? step : {};
240
+ const sPath = `${scPath}.flow[${stepIdx}]`;
241
+ const k = s.kind;
242
+ if (k === "actor") {
243
+ const aId = String(s.actor ?? "");
244
+ if (!actorIds.has(aId))
245
+ err("flow-unknown-actor", sPath, `Flow step references unknown actor id "${aId}".`);
246
+ }
247
+ else if (k === "include") {
248
+ const uId = String(s.uc ?? "");
249
+ if (!ucIds.has(uId))
250
+ err("flow-unknown-uc", sPath, `Flow step «include» references unknown UC id "${uId}".`);
251
+ // Also require a matching include relationship to exist.
252
+ const targets = includeIntoByUc.get(uId) || [];
253
+ const hasMatching = targets.some((rel) => String(rel.from) === id);
254
+ if (ucIds.has(uId) && !hasMatching) {
255
+ err("flow-include-mismatch", sPath, `Flow step «include» ${uId} from UC "${id}" does not have a matching include relationship in relationships[]. Add { kind: "include", from: "${id}", to: "${uId}" } or remove this step.`);
256
+ }
257
+ }
258
+ else if (k === "system" || k === "extension") {
259
+ // no refs to check
260
+ }
261
+ else if (k) {
262
+ err("flow-unknown-kind", sPath, `Flow step kind "${String(k)}" is not one of: actor, system, include, extension.`);
263
+ }
264
+ });
265
+ const extensions = asArray(sco.extensions);
266
+ extensions.forEach((ext, ei) => {
267
+ const e = (ext && typeof ext === "object") ? ext : {};
268
+ const at = Number(e.at);
269
+ if (!Number.isFinite(at) || at < 1 || at > flow.length) {
270
+ err("extension-bad-at", `${scPath}.extensions[${ei}]`, `Extension.at=${String(e.at)} does not reference a valid flow step (1..${flow.length}).`);
271
+ }
272
+ });
273
+ });
274
+ if (!isIncludeTarget) {
275
+ const pre = asArray(uc.preconditions);
276
+ const post = asArray(uc.postconditions);
277
+ const stories = asArray(uc.userStories);
278
+ if (pre.length === 0)
279
+ warn("uc-no-preconditions", path, `UC "${id}" has no preconditions — recommended.`);
280
+ if (post.length === 0)
281
+ warn("uc-no-postconditions", path, `UC "${id}" has no postconditions — recommended.`);
282
+ if (stories.length === 0)
283
+ err("uc-no-user-stories", path, `UC "${id}" has no userStories (§4.4). Add as many as the UC warrants — one per distinct user goal.`);
284
+ else if (stories.length === 1)
285
+ warn("uc-single-user-story", path, `UC "${id}" has only 1 user story. Add one per distinct stakeholder goal behind the UC; a single token story is usually incomplete.`);
286
+ // CR #11 — narrative content must be in Hebrew.
287
+ if (isMeaningful(uc.description) && !hasHebrew(uc.description)) {
288
+ err("non-hebrew-text", `${path}.description`, `UC description must be written in Hebrew (CR #11).`);
289
+ }
290
+ pre.forEach((p, i) => {
291
+ if (isMeaningful(p) && !hasHebrew(p))
292
+ err("non-hebrew-text", `${path}.preconditions[${i}]`, `Precondition must be written in Hebrew (CR #11).`);
293
+ });
294
+ post.forEach((p, i) => {
295
+ if (isMeaningful(p) && !hasHebrew(p))
296
+ err("non-hebrew-text", `${path}.postconditions[${i}]`, `Postcondition must be written in Hebrew (CR #11).`);
297
+ });
298
+ stories.forEach((s, i) => {
299
+ const obj = (s && typeof s === "object") ? s : {};
300
+ const combined = `${obj.role ?? ""} ${obj.want ?? ""} ${obj.so ?? ""}`;
301
+ if (isMeaningful(combined) && !hasHebrew(combined))
302
+ err("non-hebrew-text", `${path}.userStories[${i}]`, `User story must be written in Hebrew (role/want/so — CR #11).`);
303
+ });
304
+ scenarios.forEach((sc, si) => {
305
+ const sco = (sc && typeof sc === "object") ? sc : {};
306
+ const scPath = `${path}.scenarios[${si}]`;
307
+ asArray(sco.flow).forEach((step, stepIdx) => {
308
+ const s = (step && typeof step === "object") ? step : {};
309
+ if (isMeaningful(s.text) && !hasHebrew(s.text))
310
+ err("non-hebrew-text", `${scPath}.flow[${stepIdx}].text`, `Flow step text must be written in Hebrew (CR #11).`);
311
+ });
312
+ asArray(sco.extensions).forEach((ext, ei) => {
313
+ const e = (ext && typeof ext === "object") ? ext : {};
314
+ if (isMeaningful(e.text) && !hasHebrew(e.text))
315
+ err("non-hebrew-text", `${scPath}.extensions[${ei}].text`, `Extension text must be written in Hebrew (CR #11).`);
316
+ });
317
+ });
318
+ // Wireframe safety pre-check (regex-only; defense in depth — the HTML runtime
319
+ // also re-checks via wireframeSafe()).
320
+ const wf = uc.wireframe;
321
+ // §WF — a UC initiated by a human actor must have a wireframe.
322
+ if (primaryActor && actorIds.has(primaryActor)) {
323
+ const pa = actors.find((a) => String(a.id) === primaryActor);
324
+ const isHuman = !pa || pa.actorType !== "system";
325
+ const wfMissing = typeof wf !== "string" || wf.trim() === "";
326
+ if (isHuman && wfMissing) {
327
+ err("wireframe-missing", `${path}.wireframe`, `UC "${id}" has a human primary actor ("${primaryActor}") but no wireframe. Every human-initiated UC requires a wireframe per §WF. Add one, or explicitly use a placeholder describing what info is missing.`);
328
+ }
329
+ }
330
+ if (typeof wf === "string" && wf.length > 0) {
331
+ const forbidden = [
332
+ /<script\b/i,
333
+ /\son\w+\s*=/i,
334
+ /<iframe\b/i,
335
+ /src\s*=\s*["']https?:/i,
336
+ /href\s*=\s*["']https?:/i,
337
+ /@import\b/i,
338
+ /<style\b/i,
339
+ ];
340
+ const hit = forbidden.find((rx) => rx.test(wf));
341
+ if (hit) {
342
+ err("wireframe-unsafe", `${path}.wireframe`, `Wireframe HTML contains a forbidden pattern (${hit}). Remove scripts / on*= / iframe / external URLs / @import / <style>.`);
343
+ }
344
+ // CR #11 — the visible text inside the wireframe (labels, buttons, headings)
345
+ // must be in Hebrew since the product is Hebrew. A wireframe with zero
346
+ // Hebrew characters is an English UI mock.
347
+ if (!hasHebrew(wf)) {
348
+ err("wireframe-non-hebrew", `${path}.wireframe`, `Wireframe contains no Hebrew characters. Visible labels, headings, and buttons inside the wireframe must be in Hebrew (CR #11). Identifier/class names (wf-*) stay ASCII.`);
349
+ }
350
+ }
351
+ }
352
+ });
353
+ // CR #11 — assumptions / openQuestions must be Hebrew (they live at the model root).
354
+ asArray(root.assumptions).forEach((s, i) => {
355
+ if (isMeaningful(s) && !hasHebrew(s))
356
+ err("non-hebrew-text", `assumptions[${i}]`, `Assumption must be written in Hebrew (CR #11).`);
357
+ });
358
+ asArray(root.openQuestions).forEach((s, i) => {
359
+ if (isMeaningful(s) && !hasHebrew(s))
360
+ err("non-hebrew-text", `openQuestions[${i}]`, `Open question must be written in Hebrew (CR #11).`);
361
+ });
362
+ // --- Warn on too many includes (diagrams with 3+ usually have a design smell) ---
363
+ const totalIncludes = allRelationships.filter((r) => r.rel.kind === "include").length;
364
+ const totalExtends = allRelationships.filter((r) => r.rel.kind === "extend").length;
365
+ if (totalIncludes + totalExtends > 2) {
366
+ warn("many-include-extend", "relationships", `Diagram has ${totalIncludes} «include» and ${totalExtends} «extend» relationships. Most UC diagrams need ≤2 combined — review each against the §4 gate.`);
367
+ }
368
+ if (errors.length > 0)
369
+ return { ok: false, issues: errors, warnings };
370
+ return { ok: true, warnings };
371
+ }
372
+ export function parseAndValidate(jsonText) {
373
+ let parsed;
374
+ try {
375
+ parsed = JSON.parse(jsonText);
376
+ }
377
+ catch (err) {
378
+ const msg = err instanceof Error ? err.message : String(err);
379
+ return {
380
+ ok: false,
381
+ issues: [{ severity: "error", code: "invalid-json", path: "(root)", message: `JSON parse failed: ${msg}` }],
382
+ warnings: [],
383
+ };
384
+ }
385
+ return validateModel(parsed);
386
+ }
387
+ export function formatIssues(issues) {
388
+ if (issues.length === 0)
389
+ return "(no issues)";
390
+ return issues
391
+ .map((i) => `- [${i.severity === "error" ? "ERROR" : "warn"}] [${i.code}] ${i.path}: ${i.message}`)
392
+ .join("\n");
393
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sad-mcp",
3
- "version": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "MCP server for Software Analysis and Design course materials at BGU",
5
5
  "type": "module",
6
6
  "bin": {
@@ -31,6 +31,7 @@ The shared files prepended to this prompt define the layout recipes, model shape
31
31
  8. **UC labels describe actions WITH the information system, not physical real-world actions.** See §3 for the full rule and verb list.
32
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
+ 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), assumptions, and open questions must all contain Hebrew. Identifier strings (`id`, `primaryActor: 'customer'`) stay ASCII. The validator (§12) rejects narrative fields that contain zero Hebrew characters.
34
35
 
35
36
  ---
36
37
 
@@ -172,8 +173,10 @@ UC documentation: [uc-id]
172
173
  4a. if ___________ then ___________
173
174
  - id: sc-alt-1, name: "מסלול חלופי: ___" (optional additional scenarios)
174
175
 
175
- userStories:
176
+ userStories: (as many as the UC warrants — one per distinct stakeholder need; not a fixed number)
176
177
  - As a [role], I want [want], so that [benefit].
178
+ - As a [different role OR same role different goal], I want [...], so that [...].
179
+ - ... (keep adding until every user-facing goal tied to this UC is represented)
177
180
 
178
181
  wireframe?: (optional — only where the UC benefits from a UI mock; see §WF)
179
182
  ```
@@ -184,7 +187,7 @@ UC documentation: [uc-id]
184
187
  - Every `[actor:id]` in a flow step references a real actor id.
185
188
  - Every `[include:id]` in a flow step references a real UC id AND corresponds to an `«include»` relationship that passed §4.2.
186
189
  - Every `Na` extension references a valid step number `N` in the same scenario's flow.
187
- - At least one user story per UC.
190
+ - As many user stories as the UC warrants (minimum one) — cover every distinct user goal / role / scenario that motivates the UC. Do not stop at one.
188
191
 
189
192
  ### Quick disqualifiers
190
193
 
@@ -786,9 +789,15 @@ The `connections` entry for a timed link looks like:
786
789
 
787
790
  ---
788
791
 
789
- ## §WF. Wireframe Authoring (Per-UC, Optional)
792
+ ## §WF. Wireframe Authoring (Mandatory for Human-Initiated UCs)
790
793
 
791
- Where a UC benefits from a visual mock of the UI the user sees, emit a wireframe as an inline HTML fragment and attach it to `useCaseDocs[ucId].wireframe`. The modal renderer 8a) injects the fragment after passing a safety check (see `wireframeSafe()` in §8a).
794
+ **A wireframe is required for every UC whose `primaryActor.actorType === 'human'`.** The user sees a screen; a use case without a visual mock of that screen is incomplete documentation. The validator12) errors on any human-initiated UC missing a wireframe.
795
+
796
+ Wireframe is **optional** (and usually omitted) only for:
797
+ - UCs whose primary actor is a system or cron (`actorType: 'system'`) — no human sees a screen.
798
+ - Pure include-target UCs (sub-behaviors with no actor association).
799
+
800
+ For every other UC, emit a wireframe as an inline HTML fragment and attach it to `useCaseDocs[ucId].wireframe`. The modal renderer (§8a) injects the fragment after passing a safety check (see `wireframeSafe()` in §8a).
792
801
 
793
802
  Claude authors the wireframe using its own UI-generation judgment — there is no DSL. But the skill enforces a consistent look and safe sandbox via a scoped CSS theme and a set of forbidden patterns.
794
803
 
@@ -853,13 +862,14 @@ The `wireframeSafe()` regex rejects any of the following. A failing wireframe re
853
862
  - Keep each wireframe under ~200 lines of HTML. If the UC really needs more, split by scenario with `<h3 class="wf-heading">` dividers inside one `.wf-screen`.
854
863
  - Wireframes are static visual mocks. Form fields use `disabled` so the cursor shows they are illustrative.
855
864
  - Buttons are not wired to anything — `onclick` is forbidden. They communicate affordance, not behavior.
856
- - Hebrew RTL is inherited from `<body dir="rtl">`. Do not override `direction`.
865
+ - **Visible text is Hebrew (CR #11).** Every heading, label, button, table header, and hint the user sees must be in Hebrew — the product is Hebrew. Class names (`wf-*`) and identifiers stay ASCII. RTL is inherited from `<body dir="rtl">`; do not override `direction`. The validator rejects a wireframe containing zero Hebrew characters (`wireframe-non-hebrew` error).
857
866
 
858
867
  ### When NOT to include a wireframe
859
868
 
860
- - The UC has no user-facing screen (e.g., a back-end cron job, an automated reconciliation).
861
- - The UI is trivial (e.g., a single "Approve" confirmation prompt) prose in the scenario flow is enough.
862
- - You cannot author the wireframe without making up details the spec does not support — put those in `diagramModel.openQuestions` instead.
869
+ - Primary actor is a system / cron (`actorType: 'system'`) — e.g., a back-end reconciliation triggered by the clock.
870
+ - The UC is purely an `«include»` target (a sub-behavior, not a user-visible goal).
871
+
872
+ **"The UI is trivial" is not a valid reason to skip.** A single approval prompt still deserves a mock — it shows the reader what the button says, what data is visible on the screen, and what confirmation text appears. If you genuinely cannot author the wireframe because the spec is silent, record the gap in `diagramModel.openQuestions` AND still emit a placeholder wireframe with the questions visible inside it.
863
873
 
864
874
  ---
865
875
 
@@ -1149,18 +1159,44 @@ body.uc-modal-open { overflow: hidden; }
1149
1159
 
1150
1160
  ## 12. Output
1151
1161
 
1152
- - Save as `[system_name]_use_case_diagram.html`.
1153
- - Briefly state: actor count, use case count, include/extend relationships (with the justification line for each), whether split (and which trigger fired).
1162
+ **Mandatory validator step — call before writing the HTML.**
1163
+
1164
+ After assembling `diagramModel` and before generating any HTML, serialize it to JSON and call the MCP tool `uml_usecase_validate_model`:
1165
+
1166
+ ```
1167
+ uml_usecase_validate_model(model_json = JSON.stringify(diagramModel))
1168
+ ```
1169
+
1170
+ The validator runs locally (no API cost) and catches what the §4 and CR gates cannot enforce from prose:
1171
+ - Include target with an actor association (CR #4) → ERROR
1172
+ - Single-incoming «include» (CR #4) → ERROR
1173
+ - Send-only UC names like `Print Shipping Label` / `שליחת X` (CR #10) → ERROR
1174
+ - Physical-verb UC names (`Perform X`, `ביצוע X`) not wrapped with a system verb (CR #8) → WARNING
1175
+ - Primary actor missing, unknown, or not connected as initiator (§4.1/§4.4) → ERROR
1176
+ - Flow step references to unknown actor/UC ids, or `«include»` steps without a matching `include` relationship → ERROR
1177
+ - Extensions referencing invalid step numbers → ERROR
1178
+ - Narrative text missing Hebrew characters (description, preconditions, postconditions, flow/extension text, user stories, assumptions, openQuestions) → ERROR
1179
+ - Unsafe wireframe HTML (script / on*= / iframe / external URL / @import / inline style) → ERROR
1180
+ - Warnings: fewer than 2 user stories on a UC, no preconditions/postconditions, overall >2 include+extend relationships
1181
+
1182
+ If the response is `✗ Model invalid`, fix every listed error and call the validator again. **Do not write the HTML file until the validator returns `✓ Model valid`.**
1183
+
1184
+ After the validator passes, save the HTML as `[system_name]_use_case_diagram.html`.
1185
+ Briefly report: actor count, use case count, include/extend count, whether split, and any warnings the validator surfaced.
1154
1186
 
1155
1187
  ---
1156
1188
 
1157
1189
  ## 13. Pre-Delivery Checklist
1158
1190
 
1191
+ - [ ] `uml_usecase_validate_model` was called and returned `✓ Model valid` on the final model (§12). Warnings may remain but every ERROR is fixed.
1192
+ - [ ] All narrative UC documentation (description, preconditions, postconditions, flow-step text, extension text, user stories, assumptions, open questions) is in Hebrew (CR #11). Identifier strings remain ASCII.
1193
+ - [ ] User stories cover every distinct stakeholder goal behind the UC — not a single token story. If the UC serves multiple roles or scenarios, each gets its own story.
1159
1194
  - [ ] Every UC has a filled §4.4 documentation block AND a matching `useCaseDocs[id]` entry with `primaryActor`, ≥1 scenario, ≥1 flow step, `preconditions`, `postconditions`, and `userStories`.
1160
1195
  - [ ] Every `primaryActor` id exists in `actors[]` AND has an association with `role: 'initiator'` on that UC (aligning §4.1 with §4.4).
1161
1196
  - [ ] Every flow step of `kind: 'actor'` references a real actor id; every `kind: 'include'` references a real UC id; every `extension.at` references a valid step number in its scenario's flow.
1162
1197
  - [ ] Clicking a UC on the diagram opens the full-screen modal (§8a); Esc / backdrop click closes it. Step actor/UC references are clickable. Actor popup + association popup (§8b) still work for non-UC clicks.
1163
- - [ ] Any `wireframe` HTML passes `wireframeSafe()` (no `<script>`, no `on*=`, no `<iframe>`, no external URLs, no `@import`, no inline `<style>`); all classes are `wf-*` only.
1198
+ - [ ] Every UC whose primary actor is human has a wireframe (§WF). System/cron-initiated UCs and pure include-targets may omit.
1199
+ - [ ] Any `wireframe` HTML passes `wireframeSafe()` (no `<script>`, no `on*=`, no `<iframe>`, no external URLs, no `@import`, no inline `<style>`); all classes are `wf-*` only; visible text is in Hebrew.
1164
1200
  - [ ] Every actor-UC association has a written §4.1 justification block (role, initiates? one-line "why"); every UC has ≥1 association with `initiates = yes`.
1165
1201
  - [ ] Every association line carries a transparent click-hitbox (stroke-width ≥12, `data-conn-id`) wired to `showDesc(id,'assoc',event)`; `assocDescriptions` entry exists for each id.
1166
1202
  - [ ] `assumptions` and `openQuestions` arrays are present in `diagramModel`; the HTML renders the bottom panel whenever either is non-empty.