sad-mcp 2.3.0 → 2.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tools.js CHANGED
@@ -283,7 +283,7 @@ export function registerToolHandlers(server) {
283
283
  },
284
284
  {
285
285
  name: "interview_stakeholder",
286
- description: "Roleplay a stakeholder from the course's running campus-cafeteria case so a student can practise requirements-elicitation interviewing (asking 'why', 5-Whys, separating symptoms from root causes, spotting contradictions). Calling this turns YOU into the character: stay fully in character, in Hebrew first person, and reveal root causes only when the student asks good questions. Currently available: 'shimon' (שמעון, the cafeteria manager who initiated the project). The teacher steers with bracketed commands like [צא מהדמות]/[out of character] or [רמז]. Call once to summon the character, then answer every following message in character until released.",
286
+ description: "Roleplay a stakeholder from the course's running campus-cafeteria case so a student can practise requirements-elicitation interviewing (asking 'why', 5-Whys, separating symptoms from root causes, spotting contradictions). Calling this turns YOU into the character: stay fully in character, in Hebrew first person, and reveal root causes only when the student asks good questions. Currently available: 'shimon' (שמעון, the cafeteria manager who initiated the project). The teacher steers with bracketed commands like [צא מהדמות]/[out of character] or [רמז]. A student working alone ends by typing [סיכום] (or saying they're done) to get a formative, ungraded coaching debrief on their interview — coverage of the root causes, questioning technique, what they missed and how, with an option to go back in ([חזור לדמות]). Call once to summon the character, then answer every following message in character until released.",
287
287
  inputSchema: {
288
288
  type: "object",
289
289
  properties: {
@@ -348,7 +348,7 @@ export function validateModel(model) {
348
348
  const isHuman = !pa || pa.actorType !== "system";
349
349
  const wfMissing = typeof wf !== "string" || wf.trim() === "";
350
350
  if (isHuman && wfMissing) {
351
- 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.`);
351
+ warn("wireframe-missing", `${path}.wireframe`, `UC "${id}" has a human primary actor ("${primaryActor}") but no wireframe. Wireframes are opt-in per §WF add one only if the user requested wireframes.`);
352
352
  }
353
353
  }
354
354
  if (typeof wf === "string" && wf.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sad-mcp",
3
- "version": "2.3.0",
3
+ "version": "2.3.3",
4
4
  "description": "MCP server for Software Analysis and Design course materials at BGU",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,8 @@
8
8
  },
9
9
  "files": [
10
10
  "dist",
11
- "skills"
11
+ "skills",
12
+ "!**/*conflicted copy*"
12
13
  ],
13
14
  "scripts": {
14
15
  "build": "tsc",
@@ -13,8 +13,9 @@ you are**. This section tells you **how to play the role**. Follow both exactly.
13
13
  1. **Stay fully in character.** From now until the student explicitly releases you,
14
14
  you ARE this person. Speak in **first person, in Hebrew**, in the character's
15
15
  voice. Never speak as "Claude", never describe the character in third person,
16
- never narrate stage directions. If the student writes in English, you may answer
17
- in English, but stay in character.
16
+ never narrate stage directions, and never emit bracketed notes or any commentary
17
+ about your own answers (see "What leaves your mouth" below). If the student writes
18
+ in English, you may answer in English, but stay in character.
18
19
 
19
20
  2. **You are being interviewed — you do NOT run the interview.** Answer the question
20
21
  that was asked and then stop. Do **not** volunteer a tidy, organised summary of
@@ -49,6 +50,27 @@ you are**. This section tells you **how to play the role**. Follow both exactly.
49
50
  7. **Stay human and brief.** Two to five sentences per answer, usually. Use the
50
51
  character's register — colloquial, a little defensive, busy. Not a polished memo.
51
52
 
53
+ ## What leaves your mouth — nothing but the character
54
+
55
+ Everything you output during the interview is **only the character's spoken words, in
56
+ Hebrew**. Nothing else reaches the student:
57
+
58
+ - **No brackets of your own.** Bracketed text `[...]` is **input only** — it is how the
59
+ teacher or student talks *to* you. You never *produce* a bracketed note. If you catch
60
+ yourself opening a `[` to write a stage direction, stop.
61
+ - **No director's notes, no strategy narration.** Never tell the student what kind of
62
+ question they just asked, how much depth you're deciding to give, or what you're
63
+ choosing to reveal or hold back. That reasoning stays **silent**. For example, you must
64
+ NEVER emit anything like «[שאלת ה"למה" הראשונה. תן לו קצת עומק — אבל לא הכל בבת אחת,
65
+ ובלי לסכם לו יפה.]» — the student sees only Shimon's reply, never the thinking behind it.
66
+ - **No labels or meta.** No "as Shimon:", no "(חושב)", no summary of the situation, no
67
+ coaching. Just say what the character would say, then stop.
68
+
69
+ The rules in this file and the *italic bracketed notes* in the persona are for your eyes
70
+ only — act on them, never voice them. The single exception is the debrief: when the
71
+ student explicitly ends the interview (see below), you drop the role entirely and speak
72
+ as the coach.
73
+
52
74
  ## How the teacher steers the session (out-of-character commands)
53
75
 
54
76
  The teacher may send bracketed meta-instructions. These are NOT part of the
@@ -71,6 +93,55 @@ On the very first turn, give a short, in-character greeting (1–2 sentences) th
71
93
  fits the persona — busy, a little impatient — and invite the first question. Do not
72
94
  dump information. Then wait.
73
95
 
96
+ ## Ending the interview — the debrief (student self-feedback)
97
+
98
+ This tool is usually used by a student working **alone**, with no teacher in the
99
+ room. When the student signals they are **done and want feedback** — either by
100
+ typing `[סיכום]` (also accept `[משוב]` / `[debrief]`) or by clearly saying in plain
101
+ language that they've finished and want to know how they did ("סיימתי, איך הלך?",
102
+ "תן לי משוב על הראיון", "how did I do?") — **stop role-playing and answer as a SAD
103
+ course coach** (as Claude, not the character). Trigger this ONLY on a clear, explicit
104
+ end-and-feedback signal; an ordinary interview question is *not* a debrief request —
105
+ answer it in character.
106
+
107
+ The debrief is **formative and ungraded** — its only purpose is to help the student
108
+ learn requirements-elicitation by reflecting on the interview they just ran. Never
109
+ give a numeric grade or a pass/fail. Write it in **Hebrew**, addressed to the student
110
+ (פנייה ישירה), warm, specific, and practical — a coach, not an examiner. Review the
111
+ **entire conversation so far** — every question the student asked and every answer the
112
+ character gave — and build the feedback from that transcript, quoting the student's
113
+ own words. Cover, in this order:
114
+
115
+ 1. **מה חשפת (coverage).** Build a checklist from the persona's *DEEP* facts and its
116
+ *contradictions* — treat each as one root cause / insight the interview was meant to
117
+ surface. Mark which the student actually reached and which stayed buried. Frame it
118
+ as progress, not a score (e.g. "הגעת ל-4 מתוך 6 שורשי הבעיה"). For each one reached,
119
+ name the moment the student earned it.
120
+
121
+ 2. **איך תשאלת (technique).** Diagnose the elicitation skill with concrete examples
122
+ from the transcript: did they ask **"למה"** and follow up (5-Whys) or accept the
123
+ first answer? Did they **push on contradictions**? Did they **ask for evidence /
124
+ numbers** instead of taking claims at face value? Did they **separate symptom from
125
+ root**? Did they **filter noise** or chase the red herrings the character mixed in?
126
+ Praise what worked by quoting it; name what was missing by quoting where it slipped.
127
+
128
+ 3. **מה פספסת ואיך היית מגיע לזה (the teachable misses).** For each buried root cause,
129
+ do NOT just hand over the answer — show the **question that would have opened it**,
130
+ tied to the student's own words (e.g. «כשהוא אמר 'חסר לנו כוח אדם', שאלה כמו 'ומה
131
+ קרה כשהוספת עובד?' הייתה חושפת ש…»). Teach the move, not just the fact.
132
+
133
+ 4. **קדימה (forward link).** In a line or two, connect the root causes the student
134
+ uncovered to the next step of the analysis (the requirements / use-cases that follow
135
+ from them), so the interview feels like real analysis work.
136
+
137
+ 5. **רוצה לנסות שוב? (retry).** Because this is practice, end by inviting the student
138
+ to go back and probe what they missed: «רוצה לחזור ולתשאל אותו על מה שפספסת? כתוב
139
+ `[חזור לדמות]` ונמשיך מאותה נקודה.» If they do, resume the character in full, with
140
+ the whole prior interview still in mind.
141
+
142
+ Keep the debrief tight and readable — it doubles as the artifact the student hands in,
143
+ so make it clean enough to copy as-is.
144
+
74
145
  ---
75
146
 
76
147
  ## PERSONA
@@ -5,7 +5,7 @@ Source lectures: שיעורים 6-7 תרשים מחלקות.pptx, ניתוץ ש
5
5
  ## Elements Taught
6
6
 
7
7
  - **Classes** — three compartments: name, attributes, operations
8
- - **Enumerations** — `«enumeration»` stereotype; use when: closed list, textual values, ≤~20 items. Do NOT use for open-ended or numeric choices
8
+ - **Enumerations** — `«enumeration»` stereotype; use when: closed list, textual values, ≤~20 items. Do NOT use for open-ended or numeric choices. Draw as an **unconnected box** next to the class that uses it — no dependency or association line to the class.
9
9
  - **Packages (חבילה)** — grouping mechanism for related classes; taught as an organizational concept
10
10
  - **Abstract classes / interfaces** — not explicitly covered in lectures; avoid adding unless user asks
11
11
 
@@ -69,7 +69,7 @@ Source lectures: שיעורים 6-7 תרשים מחלקות.pptx, ניתוץ ש
69
69
  - Strict LEFT/CENTER/RIGHT zone layout — lectures don't prescribe this
70
70
  - Interactive composition-toggle panel — not a teaching concept
71
71
  - Enumerations placed only in a "RIGHT zone" — not explicitly taught
72
- - No dependency lines from enums to classes — not mentioned; lectures use dependencies normally
72
+ - Enumerations are drawn as **unconnected boxes** next to their using class the course does **not** draw a dependency (or any) line from a class to an enum; the attribute type reference is the only link (instructor rule, 2026)
73
73
  - Association class connected to midpoint — taught in principle but not the dashed-line-to-midpoint rule specifically
74
74
 
75
75
  ### Rules lectures teach that SKILL.md omits or misrepresents
@@ -66,6 +66,7 @@ Two common false positives:
66
66
  - Use enumerations when: closed list, textual values, ≤~20 items, will not change often
67
67
  - Do NOT use for open-ended, numeric, or frequently changing value sets
68
68
  - Place enumerations adjacent to the class that uses them, not in a fixed zone
69
+ - **Do NOT connect an enumeration to a class with any line** (no dependency, no association). The attribute type reference (`status : RegistrationStatus`) is the only link. `type: 'dep'` in RELS is not used.
69
70
 
70
71
  ### Packages (חבילה)
71
72
  - Draw as a tabbed rectangle enclosing related classes
@@ -83,11 +84,11 @@ Two common false positives:
83
84
  | Aggregation | Solid | Hollow diamond at whole | "has-a", part can exist independently |
84
85
  | Composition | Solid | Filled diamond at whole | Part cannot exist without whole |
85
86
  | Generalization | Solid | Hollow triangle at parent | True is-a; subtypes have different attributes/ops |
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
+ | Dependency | Dashed | Open arrow | **Not used in this course's class diagrams.** Enumerations are NOT connected to their using class by any line (see §2 / §3 CRITICAL). Never draw a dependency between two regular classes either — use a plain association. |
87
88
 
88
89
  > **CRITICAL — domain diagram association rules:**
89
90
  > - 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
+ > - Dependency (---→): **not used at all.** Enumerations are drawn as unconnected boxes placed next to their using class — no line of any kind. The attribute type reference (`status : RegistrationStatus`) is the only link. Do not draw a dependency between two regular classes either.
91
92
  > - Directed association (──→): never in a domain or analysis diagram.
92
93
 
93
94
  **Multiplicity at both ends of every association**: `1`, `0..1`, `1..*`, `0..*`. Never leave one end unlabeled.
@@ -484,7 +485,7 @@ When in doubt, use the male form.
484
485
  - [ ] Hebrew names use male form
485
486
  - [ ] **No raw SVG class boxes** — class boxes are rendered by `svgBox()` from `CLASSES` data, not hand-written SVG
486
487
  - [ ] **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
+ - [ ] **No dependency lines at all** — enumerations are unconnected boxes placed near their using class (no `type: 'dep'`); dependency is not used in this course's class diagrams, and never between two regular classes
488
489
  - [ ] **`<html>` has no `dir="rtl"`** — SVG coordinates are LTR; page language does not affect SVG rendering
489
490
  - [ ] **`boxH()` / `boxW()` from §9d engine used** — never hardcode box dimensions in data or SVG
490
491
  - [ ] **Attribute/operation rows: `text-anchor="start"` at `x = box.x + 6`** — enforced by engine; do not override
@@ -521,6 +521,7 @@ If neither trigger fires, produce a single tab. Do not split on gut feeling.
521
521
  - One tab per actor group or per functional area.
522
522
  - **Cross-tab connections:** draw the stub on each side with the partner's name and a `(→ Tab N)` label. Do NOT try to route a line across tabs.
523
523
  - Each tab has its own complete SVG (its own boundary, actors, ellipses, lines).
524
+ - **Shared actors (an actor that owns UCs on more than one tab) MUST have their own `<g id="actor-group-{id}">` figure emitted inside EVERY tab's SVG where they appear — not only the first tab.** An actor's stick-figure group and its association line must live in the same SVG. If a tab contains a `data-conn-from="{actorId}"` line but no matching `actor-group-{actorId}` figure in that same SVG, `fixLayout` places the line correctly but there is nothing to draw at the actor end — the association renders as a line into empty space. (`fixLayout` scopes its DOM lookups per-SVG, so the same `actor-group-{id}` id may repeat across tab SVGs; each tab still needs its own figure element.)
524
525
  - Each tab has a visible **split-rationale line** below the tab bar. Use whichever trigger actually fired:
525
526
  - `"Split by UC count: 12 UCs > 8 threshold"` or
526
527
  - `"Split by actor groups: [group A] / [group B] — disjoint responsibilities"`.
@@ -942,4 +943,5 @@ Briefly report: actor count, use case count, include/extend count, whether split
942
943
  - [ ] `showDiagram(index)` is defined in every generated file, even single-tab diagrams (so tab switching is wired if splitting is ever added).
943
944
  - [ ] `fixLayout` uses adaptive column X (widens boundary if needed so left/center/right ellipses never overlap horizontally) AND expands viewBox width so right-side actors are not clipped.
944
945
  - [ ] If split: each tab has a split-rationale line citing the actual trigger that fired (count > 8, or 2+ disjoint actor groups); `showDiagram` and per-tab `fixLayout` wired per §11.
946
+ - [ ] If split: every actor in a tab's `diagramLayouts[i].actors` has a matching `<g id="actor-group-{id}">` figure inside THAT tab's `<svg>`. A shared actor (owning UCs on multiple tabs) is duplicated into each tab's SVG — never emit its figure in only the first tab, or its association renders as a line into empty space.
945
947
  - [ ] `diagramModel` defined per `model-shape.md`; VP export button present and wired to `exportXMI()`.
@@ -1,15 +0,0 @@
1
- export interface ValidationIssue {
2
- path: string;
3
- code: string;
4
- message: string;
5
- }
6
- export type ValidateResult = {
7
- ok: true;
8
- warnings: ValidationIssue[];
9
- } | {
10
- ok: false;
11
- issues: ValidationIssue[];
12
- warnings: ValidationIssue[];
13
- };
14
- export declare function formatIssues(issues: ValidationIssue[]): string;
15
- export declare function parseAndValidate(jsonText: string): ValidateResult;
@@ -1,218 +0,0 @@
1
- // Validator for use-case diagramModel JSON (kind: 'use-case').
2
- // Called by uml_usecase_validate_model MCP tool in tools.ts.
3
- // Returns all errors and warnings at once so an LLM can self-correct in one pass.
4
- export function formatIssues(issues) {
5
- if (issues.length === 0)
6
- return "(none)";
7
- return issues.map(i => `- [${i.code}] ${i.path}: ${i.message}`).join("\n");
8
- }
9
- // ── Helpers ───────────────────────────────────────────────────────────────
10
- function hasHebrew(s) {
11
- return /[\u0590-\u05FF\uFB1D-\uFB4F]/.test(s);
12
- }
13
- const SEND_ONLY_PREFIXES = [
14
- /^שליחת /i, /^הודעה ל/i, /^הודעת /i, /^משלוח /i,
15
- /^Send /i, /^Print /i, /^Notify /i,
16
- ];
17
- const PHYSICAL_VERB_PREFIXES = [
18
- /^ביצוע /i, /^הכנת /i, /^ספירת /i, /^מסירת /i,
19
- /^תשלום ב/i, /^Perform /i, /^Execute /i,
20
- ];
21
- const UNSAFE_WF = [
22
- /<script\b/i, /\son\w+\s*=/i, /<iframe\b/i,
23
- /src\s*=\s*["']https?:/i, /href\s*=\s*["']https?:/i,
24
- /@import\b/i, /<style\b/i,
25
- ];
26
- function wireframeSafe(html) {
27
- return !UNSAFE_WF.some(rx => rx.test(html));
28
- }
29
- // ── Main validator ────────────────────────────────────────────────────────
30
- export function parseAndValidate(jsonText) {
31
- let raw;
32
- try {
33
- raw = JSON.parse(jsonText);
34
- }
35
- catch (err) {
36
- const msg = err instanceof Error ? err.message : String(err);
37
- return { ok: false, warnings: [], issues: [{ path: "(root)", code: "invalid-json", message: `JSON parse failed: ${msg}` }] };
38
- }
39
- const errors = [];
40
- const warnings = [];
41
- const err = (path, code, message) => errors.push({ path, code, message });
42
- const warn = (path, code, message) => warnings.push({ path, code, message });
43
- const m = raw;
44
- if (!m || typeof m !== "object") {
45
- err("(root)", "not-object", "model must be an object");
46
- return { ok: false, issues: errors, warnings };
47
- }
48
- if (m["kind"] !== "use-case")
49
- err("kind", "wrong-kind", `expected "use-case", got "${m["kind"]}"`);
50
- const parts = Array.isArray(m["parts"]) ? m["parts"] : [];
51
- if (!Array.isArray(m["parts"]) || parts.length === 0)
52
- err("parts", "missing-parts", "parts array is required and must be non-empty");
53
- // Narrative field checks at model level
54
- for (const [key, val] of [["assumptions", m["assumptions"]], ["openQuestions", m["openQuestions"]]]) {
55
- if (Array.isArray(val)) {
56
- val.forEach((item, i) => {
57
- if (typeof item === "string" && item.length > 0 && !hasHebrew(item)) {
58
- err(`${key}[${i}]`, "narrative-non-hebrew", `${key}[${i}] contains no Hebrew characters`);
59
- }
60
- });
61
- }
62
- }
63
- for (const [pi, part] of parts.entries()) {
64
- const pp = `parts[${pi}]`;
65
- const elements = Array.isArray(part["elements"]) ? part["elements"] : [];
66
- const relationships = Array.isArray(part["relationships"]) ? part["relationships"] : [];
67
- const actorIds = new Set();
68
- const ucIds = new Set();
69
- // Index actors and UCs
70
- for (const el of elements) {
71
- if (el["kind"] === "actor" && typeof el["id"] === "string")
72
- actorIds.add(el["id"]);
73
- if (el["kind"] === "useCase" && typeof el["id"] === "string")
74
- ucIds.add(el["id"]);
75
- }
76
- // Build relationship indexes
77
- const includesByTarget = new Map(); // targetUcId → [baseUcId...]
78
- const assocsByUc = new Map(); // ucId → [actorId...]
79
- const includeSet = new Set(); // "from:to"
80
- let totalIncludeExtend = 0;
81
- for (const rel of relationships) {
82
- if (rel["kind"] === "include") {
83
- const from = String(rel["from"] ?? ""), to = String(rel["to"] ?? "");
84
- const list = includesByTarget.get(to) ?? [];
85
- list.push(from);
86
- includesByTarget.set(to, list);
87
- includeSet.add(`${from}:${to}`);
88
- totalIncludeExtend++;
89
- }
90
- if (rel["kind"] === "extend")
91
- totalIncludeExtend++;
92
- if (rel["kind"] === "actorAssoc") {
93
- const ucId = String(rel["ucId"] ?? ""), actorId = String(rel["actorId"] ?? "");
94
- const list = assocsByUc.get(ucId) ?? [];
95
- list.push(actorId);
96
- assocsByUc.set(ucId, list);
97
- }
98
- }
99
- if (totalIncludeExtend > 2)
100
- warn(`${pp}.relationships`, "too-many-include-extend", `${totalIncludeExtend} include+extend relationships found — more than 2 is almost always wrong`);
101
- // Check «include» targets
102
- for (const [targetId, bases] of includesByTarget) {
103
- if (bases.length < 2) {
104
- err(`${pp}.relationships[include→${targetId}]`, "single-incoming-include", `UC "${targetId}" has exactly one incoming «include» (from "${bases[0]}") — fold it into the parent instead`);
105
- }
106
- if (assocsByUc.has(targetId)) {
107
- err(`${pp}.relationships[include→${targetId}]`, "include-target-has-assoc", `UC "${targetId}" is an «include» target but also has direct actor associations — it cannot be both`);
108
- }
109
- }
110
- // Check UC elements
111
- for (const el of elements) {
112
- if (el["kind"] !== "useCase")
113
- continue;
114
- const id = String(el["id"] ?? "");
115
- const name = String(el["name"] ?? "");
116
- const ePath = `${pp}.elements[id=${id}]`;
117
- // Send-only check
118
- if (SEND_ONLY_PREFIXES.some(rx => rx.test(name))) {
119
- err(ePath, "send-only-uc", `UC "${id}" name "${name}" appears to be send-only — fold the send step into the UC that decided to send`);
120
- }
121
- // Physical verb check
122
- if (PHYSICAL_VERB_PREFIXES.some(rx => rx.test(name))) {
123
- warn(ePath, "physical-verb", `UC "${id}" name "${name}" uses a physical verb — wrap with a system verb (רישום, הזנה, עדכון, סימון…)`);
124
- }
125
- // Primary actor
126
- const primaryActor = el["primaryActor"];
127
- if (!primaryActor) {
128
- err(ePath, "missing-primary-actor", `UC "${id}" has no primaryActor`);
129
- }
130
- else {
131
- if (!actorIds.has(primaryActor)) {
132
- err(ePath, "unknown-primary-actor", `UC "${id}" primaryActor "${primaryActor}" is not in actors list`);
133
- }
134
- // Check that an actorAssoc with role=initiator exists for this actor↔UC pair
135
- const initiatorAssoc = relationships.find(r => r["kind"] === "actorAssoc" && r["ucId"] === id && r["actorId"] === primaryActor && r["role"] === "initiator");
136
- if (!initiatorAssoc) {
137
- err(ePath, "primary-not-initiator", `UC "${id}" primaryActor "${primaryActor}" has no actorAssoc with role="initiator"`);
138
- }
139
- }
140
- // Narrative fields — must contain Hebrew
141
- for (const [key, val] of [["description", el["description"]]]) {
142
- if (typeof val === "string" && val.length > 0 && !hasHebrew(val)) {
143
- err(`${ePath}.${key}`, "narrative-non-hebrew", `${key} contains no Hebrew characters`);
144
- }
145
- }
146
- for (const [key, arr] of [["preconditions", el["preconditions"]], ["postconditions", el["postconditions"]]]) {
147
- if (!Array.isArray(arr) || arr.length === 0) {
148
- warn(`${ePath}.${key}`, `missing-${key}`, `UC "${id}" has no ${key}`);
149
- }
150
- else {
151
- arr.forEach((item, i) => {
152
- if (typeof item === "string" && item.length > 0 && !hasHebrew(item)) {
153
- err(`${ePath}.${key}[${i}]`, "narrative-non-hebrew", `${key}[${i}] contains no Hebrew characters`);
154
- }
155
- });
156
- }
157
- }
158
- // User stories
159
- const userStories = Array.isArray(el["userStories"]) ? el["userStories"] : [];
160
- if (userStories.length < 2) {
161
- warn(`${ePath}.userStories`, "few-user-stories", `UC "${id}" has ${userStories.length} user stor${userStories.length === 1 ? "y" : "ies"} — cover every distinct stakeholder goal`);
162
- }
163
- // Scenarios / flow steps
164
- const scenarios = Array.isArray(el["scenarios"]) ? el["scenarios"] : [];
165
- if (scenarios.length === 0) {
166
- err(`${ePath}.scenarios`, "missing-scenarios", `UC "${id}" has no scenarios`);
167
- }
168
- for (const [si, sce] of scenarios.entries()) {
169
- const sPath = `${ePath}.scenarios[${si}]`;
170
- const flow = Array.isArray(sce["flow"]) ? sce["flow"] : [];
171
- if (flow.length === 0) {
172
- err(`${sPath}.flow`, "empty-flow", `scenario "${sce["id"]}" in UC "${id}" has no flow steps`);
173
- }
174
- const flowLen = flow.length;
175
- for (const [fi, step] of flow.entries()) {
176
- const stepPath = `${sPath}.flow[${fi}]`;
177
- if (step["kind"] === "actor" && typeof step["actor"] === "string" && !actorIds.has(step["actor"])) {
178
- err(stepPath, "unknown-actor-ref", `flow step references unknown actor id "${step["actor"]}"`);
179
- }
180
- if (step["kind"] === "include") {
181
- const targetUc = String(step["uc"] ?? "");
182
- if (!ucIds.has(targetUc)) {
183
- err(stepPath, "unknown-uc-ref", `«include» step references unknown UC id "${targetUc}"`);
184
- }
185
- else if (!includeSet.has(`${id}:${targetUc}`)) {
186
- err(stepPath, "include-step-no-relationship", `«include» step references "${targetUc}" but no include relationship exists from "${id}" to "${targetUc}"`);
187
- }
188
- }
189
- if (typeof step["text"] === "string" && step["text"].length > 0 && !hasHebrew(step["text"])) {
190
- err(stepPath, "narrative-non-hebrew", "flow step text contains no Hebrew characters");
191
- }
192
- }
193
- const extensions = Array.isArray(sce["extensions"]) ? sce["extensions"] : [];
194
- for (const [ei, ext] of extensions.entries()) {
195
- const extPath = `${sPath}.extensions[${ei}]`;
196
- if (typeof ext["at"] === "number" && (ext["at"] < 1 || ext["at"] > flowLen)) {
197
- err(extPath, "invalid-extension-at", `extension "at" value ${ext["at"]} is out of range (flow has ${flowLen} steps)`);
198
- }
199
- if (typeof ext["text"] === "string" && ext["text"].length > 0 && !hasHebrew(ext["text"])) {
200
- err(extPath, "narrative-non-hebrew", "extension text contains no Hebrew characters");
201
- }
202
- }
203
- }
204
- // Wireframe safety (only when present and non-empty)
205
- if (typeof el["wireframe"] === "string" && el["wireframe"].length > 0) {
206
- if (!wireframeSafe(el["wireframe"])) {
207
- err(`${ePath}.wireframe`, "unsafe-wireframe", `wireframe for UC "${id}" contains forbidden patterns (script/on*/iframe/external URL/@import/style)`);
208
- }
209
- if (!hasHebrew(el["wireframe"])) {
210
- err(`${ePath}.wireframe`, "wireframe-non-hebrew", `wireframe for UC "${id}" contains no Hebrew characters`);
211
- }
212
- }
213
- }
214
- }
215
- if (errors.length > 0)
216
- return { ok: false, issues: errors, warnings };
217
- return { ok: true, warnings };
218
- }
@@ -1,15 +0,0 @@
1
- export interface ValidationIssue {
2
- path: string;
3
- code: string;
4
- message: string;
5
- }
6
- export type ValidateResult = {
7
- ok: true;
8
- warnings: ValidationIssue[];
9
- } | {
10
- ok: false;
11
- issues: ValidationIssue[];
12
- warnings: ValidationIssue[];
13
- };
14
- export declare function formatIssues(issues: ValidationIssue[]): string;
15
- export declare function parseAndValidate(jsonText: string): ValidateResult;