sad-mcp 1.1.8 → 2.1.0
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 +36 -0
- package/dist/usecase/index.d.ts +2 -0
- package/dist/usecase/index.js +1 -0
- package/dist/usecase/validate.d.ts +17 -0
- package/dist/usecase/validate.js +384 -0
- package/package.json +1 -1
- package/skills/_shared/export-buttons.md +53 -2
- package/skills/_shared/model-shape.md +35 -3
- package/skills/uml-use-case-diagram/NOTATION.md +29 -0
- package/skills/uml-use-case-diagram/SKILL.md +462 -18
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 @@
|
|
|
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,384 @@
|
|
|
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
|
+
if (typeof wf === "string" && wf.length > 0) {
|
|
322
|
+
const forbidden = [
|
|
323
|
+
/<script\b/i,
|
|
324
|
+
/\son\w+\s*=/i,
|
|
325
|
+
/<iframe\b/i,
|
|
326
|
+
/src\s*=\s*["']https?:/i,
|
|
327
|
+
/href\s*=\s*["']https?:/i,
|
|
328
|
+
/@import\b/i,
|
|
329
|
+
/<style\b/i,
|
|
330
|
+
];
|
|
331
|
+
const hit = forbidden.find((rx) => rx.test(wf));
|
|
332
|
+
if (hit) {
|
|
333
|
+
err("wireframe-unsafe", `${path}.wireframe`, `Wireframe HTML contains a forbidden pattern (${hit}). Remove scripts / on*= / iframe / external URLs / @import / <style>.`);
|
|
334
|
+
}
|
|
335
|
+
// CR #11 — the visible text inside the wireframe (labels, buttons, headings)
|
|
336
|
+
// must be in Hebrew since the product is Hebrew. A wireframe with zero
|
|
337
|
+
// Hebrew characters is an English UI mock.
|
|
338
|
+
if (!hasHebrew(wf)) {
|
|
339
|
+
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.`);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
// CR #11 — assumptions / openQuestions must be Hebrew (they live at the model root).
|
|
345
|
+
asArray(root.assumptions).forEach((s, i) => {
|
|
346
|
+
if (isMeaningful(s) && !hasHebrew(s))
|
|
347
|
+
err("non-hebrew-text", `assumptions[${i}]`, `Assumption must be written in Hebrew (CR #11).`);
|
|
348
|
+
});
|
|
349
|
+
asArray(root.openQuestions).forEach((s, i) => {
|
|
350
|
+
if (isMeaningful(s) && !hasHebrew(s))
|
|
351
|
+
err("non-hebrew-text", `openQuestions[${i}]`, `Open question must be written in Hebrew (CR #11).`);
|
|
352
|
+
});
|
|
353
|
+
// --- Warn on too many includes (diagrams with 3+ usually have a design smell) ---
|
|
354
|
+
const totalIncludes = allRelationships.filter((r) => r.rel.kind === "include").length;
|
|
355
|
+
const totalExtends = allRelationships.filter((r) => r.rel.kind === "extend").length;
|
|
356
|
+
if (totalIncludes + totalExtends > 2) {
|
|
357
|
+
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.`);
|
|
358
|
+
}
|
|
359
|
+
if (errors.length > 0)
|
|
360
|
+
return { ok: false, issues: errors, warnings };
|
|
361
|
+
return { ok: true, warnings };
|
|
362
|
+
}
|
|
363
|
+
export function parseAndValidate(jsonText) {
|
|
364
|
+
let parsed;
|
|
365
|
+
try {
|
|
366
|
+
parsed = JSON.parse(jsonText);
|
|
367
|
+
}
|
|
368
|
+
catch (err) {
|
|
369
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
370
|
+
return {
|
|
371
|
+
ok: false,
|
|
372
|
+
issues: [{ severity: "error", code: "invalid-json", path: "(root)", message: `JSON parse failed: ${msg}` }],
|
|
373
|
+
warnings: [],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return validateModel(parsed);
|
|
377
|
+
}
|
|
378
|
+
export function formatIssues(issues) {
|
|
379
|
+
if (issues.length === 0)
|
|
380
|
+
return "(no issues)";
|
|
381
|
+
return issues
|
|
382
|
+
.map((i) => `- [${i.severity === "error" ? "ERROR" : "warn"}] [${i.code}] ${i.path}: ${i.message}`)
|
|
383
|
+
.join("\n");
|
|
384
|
+
}
|
package/package.json
CHANGED
|
@@ -134,12 +134,63 @@ function xmiBody(elements, rels) {
|
|
|
134
134
|
```
|
|
135
135
|
|
|
136
136
|
### Use-case xmiBody
|
|
137
|
+
|
|
138
|
+
v2 emits the rich per-UC documentation (preconditions, postconditions, flow of events) as nested UML elements so Visual Paradigm's Specification pane populates on import.
|
|
139
|
+
|
|
137
140
|
```javascript
|
|
138
141
|
function xmiBody(elements, rels) {
|
|
139
142
|
const lines = [];
|
|
143
|
+
|
|
144
|
+
// Helper: serialize scenarios to a numbered plain-text body for OpaqueBehavior.
|
|
145
|
+
function flowText(uc) {
|
|
146
|
+
const actorName = id => (elements.find(e => e.kind === 'actor' && e.id === id) || {}).name || id;
|
|
147
|
+
const ucName = id => (elements.find(e => e.kind === 'useCase' && e.id === id) || {}).name || id;
|
|
148
|
+
const lines = [];
|
|
149
|
+
(uc.scenarios || []).forEach(sc => {
|
|
150
|
+
lines.push((sc.name || sc.id) + ':');
|
|
151
|
+
(sc.flow || []).forEach((step, i) => {
|
|
152
|
+
const n = i + 1;
|
|
153
|
+
if (step.kind === 'actor') lines.push(` ${n}. ${actorName(step.actor)}: ${step.text}`);
|
|
154
|
+
else if (step.kind === 'system') lines.push(` ${n}. System: ${step.text}`);
|
|
155
|
+
else if (step.kind === 'include') lines.push(` ${n}. «include» ${ucName(step.uc)}${step.text ? ' — ' + step.text : ''}`);
|
|
156
|
+
else if (step.kind === 'extension') lines.push(` ${n}a. ${step.text}`);
|
|
157
|
+
(sc.extensions || []).filter(e => e.at === n).forEach((e, j) => {
|
|
158
|
+
const letter = String.fromCharCode(97 + j);
|
|
159
|
+
lines.push(` ${n}${letter}. ${e.text}`);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
return lines.join('\n');
|
|
164
|
+
}
|
|
165
|
+
|
|
140
166
|
for (const el of elements) {
|
|
141
|
-
if (el.kind === 'useCase')
|
|
142
|
-
|
|
167
|
+
if (el.kind === 'useCase') {
|
|
168
|
+
const hasDocs = (el.preconditions && el.preconditions.length)
|
|
169
|
+
|| (el.postconditions && el.postconditions.length)
|
|
170
|
+
|| (el.scenarios && el.scenarios.length);
|
|
171
|
+
if (!hasDocs) {
|
|
172
|
+
lines.push(` <packagedElement xmi:type="uml:UseCase" xmi:id="${el.id}" name="${xmlEscape(el.name)}"/>`);
|
|
173
|
+
} else {
|
|
174
|
+
lines.push(` <packagedElement xmi:type="uml:UseCase" xmi:id="${el.id}" name="${xmlEscape(el.name)}">`);
|
|
175
|
+
(el.preconditions || []).forEach((p, i) => {
|
|
176
|
+
lines.push(` <precondition xmi:type="uml:Constraint" xmi:id="${el.id}-pre-${i+1}" name="pre-${i+1}">`);
|
|
177
|
+
lines.push(` <specification xmi:type="uml:OpaqueExpression" xmi:id="${el.id}-pre-${i+1}-s"><body>${xmlEscape(p)}</body></specification>`);
|
|
178
|
+
lines.push(` </precondition>`);
|
|
179
|
+
});
|
|
180
|
+
(el.postconditions || []).forEach((p, i) => {
|
|
181
|
+
lines.push(` <postcondition xmi:type="uml:Constraint" xmi:id="${el.id}-post-${i+1}" name="post-${i+1}">`);
|
|
182
|
+
lines.push(` <specification xmi:type="uml:OpaqueExpression" xmi:id="${el.id}-post-${i+1}-s"><body>${xmlEscape(p)}</body></specification>`);
|
|
183
|
+
lines.push(` </postcondition>`);
|
|
184
|
+
});
|
|
185
|
+
if (el.scenarios && el.scenarios.length) {
|
|
186
|
+
const body = flowText(el);
|
|
187
|
+
lines.push(` <ownedBehavior xmi:type="uml:OpaqueBehavior" xmi:id="${el.id}-flow" name="Flow of Events">`);
|
|
188
|
+
lines.push(` <body>${xmlEscape(body)}</body>`);
|
|
189
|
+
lines.push(` </ownedBehavior>`);
|
|
190
|
+
}
|
|
191
|
+
lines.push(` </packagedElement>`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
143
194
|
if (el.kind === 'actor')
|
|
144
195
|
lines.push(` <packagedElement xmi:type="uml:Actor" xmi:id="${el.id}" name="${xmlEscape(el.name)}"/>`);
|
|
145
196
|
}
|
|
@@ -63,6 +63,8 @@ const diagramModel = {
|
|
|
63
63
|
|
|
64
64
|
### Use-case diagram elements
|
|
65
65
|
|
|
66
|
+
**v2 schema** — every UC carries full Visual Paradigm-style documentation (primary actor, scenarios with flow-of-events, pre/post-conditions, user stories, optional wireframe). The doc fields populate the per-UC modal and the XMI export. Required: `primaryActor`, `scenarios` (≥1 with ≥1 flow step), `preconditions`, `postconditions`, `userStories`. Optional: `secondaryActors`, `wireframe`.
|
|
67
|
+
|
|
66
68
|
```javascript
|
|
67
69
|
// Element subtypes for kind: 'use-case'
|
|
68
70
|
{ kind: 'actor', id, name,
|
|
@@ -70,13 +72,43 @@ const diagramModel = {
|
|
|
70
72
|
side: 'left' | 'right',
|
|
71
73
|
x, y }
|
|
72
74
|
|
|
73
|
-
{ kind: 'useCase', id, name,
|
|
74
|
-
|
|
75
|
+
{ kind: 'useCase', id, name,
|
|
76
|
+
description: '', // short summary
|
|
77
|
+
|
|
78
|
+
// v2 documentation fields:
|
|
79
|
+
primaryActor: 'actorId', // exactly one, must exist in elements
|
|
80
|
+
secondaryActors: [], // optional actor ids
|
|
81
|
+
preconditions: [ 'string' ],
|
|
82
|
+
postconditions: [ 'string' ],
|
|
83
|
+
|
|
84
|
+
scenarios: [ // ≥1 scenario required
|
|
85
|
+
{ id, name,
|
|
86
|
+
flow: [ // ≥1 step required
|
|
87
|
+
{ kind: 'actor', actor: 'actorId', text: '' },
|
|
88
|
+
{ kind: 'system', text: '' },
|
|
89
|
+
{ kind: 'include', uc: 'ucId', text: '' },
|
|
90
|
+
{ kind: 'extension', at: 2, text: '' } // at = step number extended
|
|
91
|
+
],
|
|
92
|
+
extensions: [ // optional, alternative to inline extension steps
|
|
93
|
+
{ at: 2, text: '' }
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
|
|
98
|
+
userStories: [
|
|
99
|
+
{ role: '', want: '', so: '' }
|
|
100
|
+
],
|
|
101
|
+
|
|
102
|
+
wireframe: '', // optional inline HTML fragment, see SKILL.md §WF
|
|
103
|
+
|
|
104
|
+
x, y, rx, ry } // rx/ry = ellipse radii (diagram layout)
|
|
75
105
|
|
|
76
106
|
{ kind: 'system', id, name, x, y, w, h } // system boundary rectangle
|
|
77
107
|
|
|
78
108
|
// Relationship subtypes
|
|
79
|
-
{ kind: 'actorAssoc', id, actorId, ucId
|
|
109
|
+
{ kind: 'actorAssoc', id, actorId, ucId,
|
|
110
|
+
role: 'initiator' | 'participant' | 'recipient' | 'supplier',
|
|
111
|
+
description: '' } // click-popup text
|
|
80
112
|
{ kind: 'include', id, from, to } // from = including UC, to = included
|
|
81
113
|
{ kind: 'extend', id, from, to, extensionPoint: '' }
|
|
82
114
|
{ kind: 'generalization',id, from, to } // actors or UCs; from = child, to = parent
|
|
@@ -206,6 +206,35 @@ Reason: Critical for consistency in distributed/international teams
|
|
|
206
206
|
|
|
207
207
|
---
|
|
208
208
|
|
|
209
|
+
## 11a. UC Documentation (Visual Paradigm convention)
|
|
210
|
+
|
|
211
|
+
Beyond the diagram shapes, each use case carries a **written specification** — this is what Visual Paradigm stores in the UC's "Specification" pane and what students include in exam answers. The skill's §4.4 block captures the same structure:
|
|
212
|
+
|
|
213
|
+
| Field | VP name | Convention |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| Primary actor | Primary actor | The actor who **initiates** the UC (single). Secondary actors listed separately. |
|
|
216
|
+
| Description | Description | One-sentence summary of the goal. |
|
|
217
|
+
| Preconditions | Preconditions | Bulleted list — must hold BEFORE the UC starts. |
|
|
218
|
+
| Postconditions | Postconditions | Bulleted list — must hold AFTER the UC completes successfully. |
|
|
219
|
+
| Flow of events | Flow of Events / Basic Flow | Numbered steps. Each step names who does what. Actor references and include/extend cross-references are explicit, like pseudo-code. |
|
|
220
|
+
| Extensions | Extensions / Alternative Flows | Sub-steps labelled `Na`, `Nb`, … under the step they branch from. |
|
|
221
|
+
| User stories | (project-level, not per-UC in VP) | Added by the skill so students can trace each UC back to its requirement. |
|
|
222
|
+
|
|
223
|
+
**Flow-of-events step style** — taught by the professor (Lecture 4, slides 210-215, coffee shop example):
|
|
224
|
+
|
|
225
|
+
```
|
|
226
|
+
1. Customer selects a product from the catalog.
|
|
227
|
+
2. System adds the product to the cart.
|
|
228
|
+
3. «include» Authenticate Customer.
|
|
229
|
+
4. Customer confirms the order.
|
|
230
|
+
4a. If payment method is rejected — notify customer and end in "Not Completed" status.
|
|
231
|
+
5. System saves the order and returns an order ID.
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The in-HTML modal (§8a of the SKILL) renders this style verbatim.
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
209
238
|
## 11. SUMMARY
|
|
210
239
|
|
|
211
240
|
Professor teaches UML use case notation through:
|
|
@@ -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
|
|
|
@@ -143,6 +144,51 @@ Arrow: dashed, open arrowhead, `Base UC → Included UC`.
|
|
|
143
144
|
|
|
144
145
|
Arrow: dashed, open arrowhead, `Extension UC → Base UC`. Use at most once or twice per diagram.
|
|
145
146
|
|
|
147
|
+
### 4.4 UC documentation block (mandatory)
|
|
148
|
+
|
|
149
|
+
**Every UC on the diagram also carries full Visual Paradigm-style documentation.** This is not justification text — it is the data that populates `useCaseDocs[id]` (see §9) and is rendered inside the per-UC modal (see §8). Write one block per UC in your planning before emitting the HTML.
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
UC documentation: [uc-id]
|
|
153
|
+
name: _______
|
|
154
|
+
primaryActor: _______ (must be an actor connected to this UC with role: 'initiator' per §4.1)
|
|
155
|
+
secondaryActors: [...] (actors with role: 'participant' / 'recipient' / 'supplier')
|
|
156
|
+
description: one-sentence summary of what the UC does
|
|
157
|
+
|
|
158
|
+
preconditions:
|
|
159
|
+
- ___________________
|
|
160
|
+
- ___________________
|
|
161
|
+
postconditions:
|
|
162
|
+
- ___________________
|
|
163
|
+
|
|
164
|
+
scenarios:
|
|
165
|
+
- id: sc-happy, name: "מסלול מרכזי (Happy Path)"
|
|
166
|
+
flow:
|
|
167
|
+
1. [actor:primaryActor] ___________________
|
|
168
|
+
2. [system] ___________________
|
|
169
|
+
3. [include:uc-xxx] ___________________ (only if §4.2 gate passed)
|
|
170
|
+
4. [actor:secondary] ___________________
|
|
171
|
+
extensions:
|
|
172
|
+
2a. if ___________ then ___________
|
|
173
|
+
4a. if ___________ then ___________
|
|
174
|
+
- id: sc-alt-1, name: "מסלול חלופי: ___" (optional additional scenarios)
|
|
175
|
+
|
|
176
|
+
userStories: (as many as the UC warrants — one per distinct stakeholder need; not a fixed number)
|
|
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)
|
|
180
|
+
|
|
181
|
+
wireframe?: (optional — only where the UC benefits from a UI mock; see §WF)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Gates enforced before publishing:**
|
|
185
|
+
- Exactly one `primaryActor`; the actor exists in `actors[]` AND an association exists with `role: 'initiator'` on this UC (§4.1).
|
|
186
|
+
- At least one scenario with at least one flow step.
|
|
187
|
+
- Every `[actor:id]` in a flow step references a real actor id.
|
|
188
|
+
- Every `[include:id]` in a flow step references a real UC id AND corresponds to an `«include»` relationship that passed §4.2.
|
|
189
|
+
- Every `Na` extension references a valid step number `N` in the same scenario's flow.
|
|
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.
|
|
191
|
+
|
|
146
192
|
### Quick disqualifiers
|
|
147
193
|
|
|
148
194
|
- Exactly one incoming «include» arrow → fold into the parent.
|
|
@@ -432,15 +478,26 @@ If neither trigger fires, produce a single tab. Do not split on gut feeling.
|
|
|
432
478
|
|
|
433
479
|
---
|
|
434
480
|
|
|
435
|
-
## 8. Clickable Descriptions for UCs, Actors
|
|
481
|
+
## 8. Clickable Descriptions — Modal for UCs, Popups for Actors & Associations
|
|
436
482
|
|
|
437
|
-
|
|
483
|
+
v2 splits the click experience in two:
|
|
484
|
+
|
|
485
|
+
- **UCs → full-screen modal** showing the complete §4.4 documentation (id, name, primary actor, description, scenarios with flow-of-events, pre/post-conditions, user stories, optional wireframe). See §8a.
|
|
486
|
+
- **Actors and association lines → small floating popup** (single-line name + description), unchanged from v1.1.8. See §8b.
|
|
487
|
+
|
|
488
|
+
One entry point — `showDesc(id, type, event)` — routes to the right UI:
|
|
489
|
+
|
|
490
|
+
```javascript
|
|
491
|
+
function showDesc(id, type, event) {
|
|
492
|
+
if (type === 'uc') { openUcModal(id); event.stopPropagation(); return; }
|
|
493
|
+
// Non-UC types use the small popup (§8b)
|
|
494
|
+
showPopup(id, type, event);
|
|
495
|
+
}
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### 8b. Small popup for actors and associations
|
|
438
499
|
|
|
439
500
|
```javascript
|
|
440
|
-
const useCaseDescriptions = {
|
|
441
|
-
'uc1': { name: 'פתיחת הזמנה', desc: 'המשתמש פותח הזמנה חדשה ומציין פרטי המוצרים הנדרשים.' },
|
|
442
|
-
// ... one entry per UC
|
|
443
|
-
};
|
|
444
501
|
const actorDescriptions = {
|
|
445
502
|
'customer': { name: 'לקוח', desc: 'לקוח הרשום במערכת. יוזם הזמנות ועוקב אחר סטטוס.' },
|
|
446
503
|
// ... one entry per actor
|
|
@@ -451,10 +508,9 @@ const assocDescriptions = {
|
|
|
451
508
|
// ... one entry per association line you want described
|
|
452
509
|
};
|
|
453
510
|
|
|
454
|
-
function
|
|
511
|
+
function showPopup(id, type, event) {
|
|
455
512
|
hideDesc();
|
|
456
|
-
const data = type === '
|
|
457
|
-
: type === 'actor' ? actorDescriptions[id]
|
|
513
|
+
const data = type === 'actor' ? actorDescriptions[id]
|
|
458
514
|
: type === 'assoc' ? assocDescriptions[id]
|
|
459
515
|
: null;
|
|
460
516
|
if (!data) return;
|
|
@@ -486,6 +542,166 @@ document.addEventListener('click', e => {
|
|
|
486
542
|
});
|
|
487
543
|
```
|
|
488
544
|
|
|
545
|
+
### 8a. Full-screen modal for UCs
|
|
546
|
+
|
|
547
|
+
Every UC ellipse's click routes to `openUcModal(id)`, which reads `useCaseDocs[id]` (populated from the §4.4 documentation blocks) and renders a full-screen modal with the complete UC spec. The modal has sections for: header (id + name + primary actor link), description, scenarios (numbered flow with typed steps + extensions), a row with preconditions / postconditions / user stories, and an optional wireframe.
|
|
548
|
+
|
|
549
|
+
```javascript
|
|
550
|
+
const useCaseDocs = {
|
|
551
|
+
'uc-place-order': {
|
|
552
|
+
id: 'uc-place-order',
|
|
553
|
+
name: 'פתיחת הזמנה',
|
|
554
|
+
primaryActor: 'customer',
|
|
555
|
+
secondaryActors: ['inventory'],
|
|
556
|
+
description: 'הלקוח פותח הזמנה חדשה ומציין פריטים.',
|
|
557
|
+
preconditions: ['הלקוח מחובר למערכת.'],
|
|
558
|
+
postconditions: ['ההזמנה נשמרה עם סטטוס "ממתינה".'],
|
|
559
|
+
scenarios: [
|
|
560
|
+
{ id: 'sc-happy', name: 'מסלול מרכזי',
|
|
561
|
+
flow: [
|
|
562
|
+
{ kind: 'actor', actor: 'customer', text: 'בוחר פריט מהקטלוג' },
|
|
563
|
+
{ kind: 'system', text: 'מוסיף את הפריט לעגלה' },
|
|
564
|
+
{ kind: 'include', uc: 'uc-authenticate', text: '' },
|
|
565
|
+
{ kind: 'actor', actor: 'customer', text: 'מאשר את ההזמנה' }
|
|
566
|
+
],
|
|
567
|
+
extensions: [
|
|
568
|
+
{ at: 1, text: 'אם הפריט אזל במלאי — מציג הודעה וחוזר לשלב 1.' }
|
|
569
|
+
]
|
|
570
|
+
}
|
|
571
|
+
],
|
|
572
|
+
userStories: [
|
|
573
|
+
{ role: 'לקוח', want: 'להזמין מוצר במהירות', so: 'שאקבל אותו תוך 48 שעות.' }
|
|
574
|
+
],
|
|
575
|
+
wireframe: '' // optional per §WF
|
|
576
|
+
}
|
|
577
|
+
// ... one entry per UC
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
function openUcModal(id) {
|
|
581
|
+
const doc = useCaseDocs[id];
|
|
582
|
+
if (!doc) return;
|
|
583
|
+
const actorName = aid => (actorDescriptions[aid]?.name) || aid;
|
|
584
|
+
const ucName = uid => (useCaseDocs[uid]?.name) || uid;
|
|
585
|
+
const modal = document.getElementById('uc-modal');
|
|
586
|
+
modal.querySelector('.uc-id').textContent = doc.id;
|
|
587
|
+
modal.querySelector('.uc-name').textContent = doc.name;
|
|
588
|
+
const paLink = modal.querySelector('.uc-primary-link');
|
|
589
|
+
paLink.textContent = actorName(doc.primaryActor);
|
|
590
|
+
paLink.dataset.actorId = doc.primaryActor;
|
|
591
|
+
paLink.onclick = () => { closeUcModal(); pulseActor(doc.primaryActor); };
|
|
592
|
+
|
|
593
|
+
modal.querySelector('.uc-description').textContent = doc.description || '';
|
|
594
|
+
|
|
595
|
+
// Scenarios
|
|
596
|
+
const sc = modal.querySelector('.uc-scenarios');
|
|
597
|
+
sc.innerHTML = '';
|
|
598
|
+
(doc.scenarios || []).forEach(sce => {
|
|
599
|
+
const h = document.createElement('h3'); h.textContent = sce.name || sce.id; sc.appendChild(h);
|
|
600
|
+
const ol = document.createElement('ol');
|
|
601
|
+
(sce.flow || []).forEach((step, i) => {
|
|
602
|
+
const li = document.createElement('li');
|
|
603
|
+
if (step.kind === 'actor') {
|
|
604
|
+
const strong = document.createElement('strong');
|
|
605
|
+
strong.className = 'step-actor';
|
|
606
|
+
strong.dataset.actorId = step.actor;
|
|
607
|
+
strong.textContent = actorName(step.actor);
|
|
608
|
+
strong.onclick = () => { closeUcModal(); pulseActor(step.actor); };
|
|
609
|
+
li.appendChild(strong);
|
|
610
|
+
li.appendChild(document.createTextNode(': ' + (step.text || '')));
|
|
611
|
+
} else if (step.kind === 'system') {
|
|
612
|
+
const em = document.createElement('em');
|
|
613
|
+
em.className = 'step-system'; em.textContent = 'המערכת';
|
|
614
|
+
li.appendChild(em);
|
|
615
|
+
li.appendChild(document.createTextNode(': ' + (step.text || '')));
|
|
616
|
+
} else if (step.kind === 'include') {
|
|
617
|
+
const span = document.createElement('span');
|
|
618
|
+
span.className = 'step-include'; span.textContent = '«include» ';
|
|
619
|
+
const a = document.createElement('a');
|
|
620
|
+
a.className = 'step-uc-ref'; a.textContent = ucName(step.uc);
|
|
621
|
+
a.onclick = () => openUcModal(step.uc);
|
|
622
|
+
li.appendChild(span); li.appendChild(a);
|
|
623
|
+
if (step.text) li.appendChild(document.createTextNode(' — ' + step.text));
|
|
624
|
+
} else if (step.kind === 'extension') {
|
|
625
|
+
li.className = 'ext-inline';
|
|
626
|
+
li.textContent = (step.at ? step.at + 'a. ' : '') + (step.text || '');
|
|
627
|
+
}
|
|
628
|
+
ol.appendChild(li);
|
|
629
|
+
// Inline extensions defined in sce.extensions anchored to this step number:
|
|
630
|
+
(sce.extensions || []).filter(e => e.at === i + 1).forEach((ext, j) => {
|
|
631
|
+
const sub = document.createElement('li');
|
|
632
|
+
sub.className = 'ext-sub';
|
|
633
|
+
const letter = String.fromCharCode(97 + j); // a, b, c, ...
|
|
634
|
+
sub.textContent = (i + 1) + letter + '. ' + ext.text;
|
|
635
|
+
ol.appendChild(sub);
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
sc.appendChild(ol);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
const fillList = (selector, items) => {
|
|
642
|
+
const el = modal.querySelector(selector);
|
|
643
|
+
el.innerHTML = '';
|
|
644
|
+
(items || []).forEach(s => { const li = document.createElement('li'); li.textContent = s; el.appendChild(li); });
|
|
645
|
+
};
|
|
646
|
+
fillList('.uc-pre ul', doc.preconditions);
|
|
647
|
+
fillList('.uc-post ul', doc.postconditions);
|
|
648
|
+
const usList = modal.querySelector('.uc-stories ul');
|
|
649
|
+
usList.innerHTML = '';
|
|
650
|
+
(doc.userStories || []).forEach(us => {
|
|
651
|
+
const li = document.createElement('li');
|
|
652
|
+
li.textContent = `As a ${us.role}, I want ${us.want}, so that ${us.so}`;
|
|
653
|
+
usList.appendChild(li);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// Wireframe — safety-checked before insertion
|
|
657
|
+
const wf = modal.querySelector('.uc-wireframe');
|
|
658
|
+
wf.innerHTML = '';
|
|
659
|
+
if (doc.wireframe && wireframeSafe(doc.wireframe)) {
|
|
660
|
+
wf.innerHTML = doc.wireframe;
|
|
661
|
+
wf.hidden = false;
|
|
662
|
+
} else {
|
|
663
|
+
wf.hidden = true;
|
|
664
|
+
if (doc.wireframe) console.warn(`Wireframe for ${id} failed safety check.`);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
modal.hidden = false;
|
|
668
|
+
document.body.classList.add('uc-modal-open');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function closeUcModal() {
|
|
672
|
+
const modal = document.getElementById('uc-modal');
|
|
673
|
+
modal.hidden = true;
|
|
674
|
+
document.body.classList.remove('uc-modal-open');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Esc closes the modal.
|
|
678
|
+
document.addEventListener('keydown', e => {
|
|
679
|
+
if (e.key === 'Escape') closeUcModal();
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
// Highlight an actor on the diagram for ~2s.
|
|
683
|
+
function pulseActor(actorId) {
|
|
684
|
+
const g = document.getElementById('actor-group-' + actorId);
|
|
685
|
+
if (!g) return;
|
|
686
|
+
g.classList.add('actor-pulse');
|
|
687
|
+
setTimeout(() => g.classList.remove('actor-pulse'), 2000);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Wireframe sanity check — see §WF. Rejects scripts, event handlers, external URLs.
|
|
691
|
+
function wireframeSafe(html) {
|
|
692
|
+
const forbidden = [
|
|
693
|
+
/<script\b/i,
|
|
694
|
+
/\son\w+\s*=/i,
|
|
695
|
+
/<iframe\b/i,
|
|
696
|
+
/src\s*=\s*["']https?:/i,
|
|
697
|
+
/href\s*=\s*["']https?:/i,
|
|
698
|
+
/@import\b/i,
|
|
699
|
+
/<style\b/i
|
|
700
|
+
];
|
|
701
|
+
return !forbidden.some(rx => rx.test(html));
|
|
702
|
+
}
|
|
703
|
+
```
|
|
704
|
+
|
|
489
705
|
UC group: `<g id="uc-uc1" data-uc-id="uc1" onclick="showDesc('uc1','uc',event)" style="cursor:pointer">`
|
|
490
706
|
Actor group: `<g id="actor-student" data-actor-id="student" onclick="showDesc('student','actor',event)" style="cursor:pointer">`
|
|
491
707
|
|
|
@@ -573,10 +789,89 @@ The `connections` entry for a timed link looks like:
|
|
|
573
789
|
|
|
574
790
|
---
|
|
575
791
|
|
|
792
|
+
## §WF. Wireframe Authoring (Per-UC, Optional)
|
|
793
|
+
|
|
794
|
+
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).
|
|
795
|
+
|
|
796
|
+
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.
|
|
797
|
+
|
|
798
|
+
### Root element
|
|
799
|
+
|
|
800
|
+
```html
|
|
801
|
+
<div class="wf-screen">
|
|
802
|
+
<!-- optional header -->
|
|
803
|
+
<h3 class="wf-heading">כותרת המסך</h3>
|
|
804
|
+
|
|
805
|
+
<!-- form rows -->
|
|
806
|
+
<div class="wf-row">
|
|
807
|
+
<label class="wf-label">שם משתמש</label>
|
|
808
|
+
<input class="wf-input" disabled placeholder="" />
|
|
809
|
+
</div>
|
|
810
|
+
<div class="wf-row">
|
|
811
|
+
<label class="wf-label">סיסמה</label>
|
|
812
|
+
<input class="wf-input" disabled placeholder="" />
|
|
813
|
+
</div>
|
|
814
|
+
|
|
815
|
+
<!-- a table mock -->
|
|
816
|
+
<table class="wf-table">
|
|
817
|
+
<thead><tr><th>פריט</th><th>כמות</th><th>מחיר</th></tr></thead>
|
|
818
|
+
<tbody>
|
|
819
|
+
<tr><td>...</td><td>...</td><td>...</td></tr>
|
|
820
|
+
</tbody>
|
|
821
|
+
</table>
|
|
822
|
+
|
|
823
|
+
<!-- an image placeholder -->
|
|
824
|
+
<div class="wf-image-box">[לוגו / תמונה]</div>
|
|
825
|
+
|
|
826
|
+
<!-- buttons are inert visual placeholders only -->
|
|
827
|
+
<div class="wf-actions">
|
|
828
|
+
<button class="wf-button wf-primary">אישור</button>
|
|
829
|
+
<button class="wf-button">ביטול</button>
|
|
830
|
+
</div>
|
|
831
|
+
|
|
832
|
+
<!-- free-text hints / callouts -->
|
|
833
|
+
<p class="wf-hint">לחיצה על "אישור" שולחת את הטופס ועוברת למסך הסיכום.</p>
|
|
834
|
+
</div>
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
### Allowed CSS classes
|
|
838
|
+
|
|
839
|
+
Use only `wf-*` classes. The skill ships a scoped theme (§10) covering: `wf-screen`, `wf-heading`, `wf-row`, `wf-label`, `wf-input`, `wf-textarea`, `wf-select`, `wf-button`, `wf-primary`, `wf-actions`, `wf-table`, `wf-image-box`, `wf-hint`, `wf-section`, `wf-badge`.
|
|
840
|
+
|
|
841
|
+
### Hard forbidden patterns
|
|
842
|
+
|
|
843
|
+
The `wireframeSafe()` regex rejects any of the following. A failing wireframe renders a placeholder and logs a warning — it is NEVER injected.
|
|
844
|
+
|
|
845
|
+
| Forbidden | Why |
|
|
846
|
+
|---|---|
|
|
847
|
+
| `<script>` | Can execute arbitrary JS in the host page. |
|
|
848
|
+
| `on*=` attributes (`onclick`, `onerror`, …) | Same. |
|
|
849
|
+
| `<iframe>` | Can load arbitrary content. |
|
|
850
|
+
| `src="http…"` / `href="http…"` | No external fetches — the HTML must remain self-contained. |
|
|
851
|
+
| `@import` | Same reason. |
|
|
852
|
+
| `<style>` block inside the wireframe | Styles come from the skill's `.wf-*` theme only, to keep wireframes visually consistent across UCs. |
|
|
853
|
+
|
|
854
|
+
### Size and scope
|
|
855
|
+
|
|
856
|
+
- 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`.
|
|
857
|
+
- Wireframes are static visual mocks. Form fields use `disabled` so the cursor shows they are illustrative.
|
|
858
|
+
- Buttons are not wired to anything — `onclick` is forbidden. They communicate affordance, not behavior.
|
|
859
|
+
- **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).
|
|
860
|
+
|
|
861
|
+
### When NOT to include a wireframe
|
|
862
|
+
|
|
863
|
+
- The UC has no user-facing screen (e.g., a back-end cron job, an automated reconciliation).
|
|
864
|
+
- The UI is trivial (e.g., a single "Approve" confirmation prompt) — prose in the scenario flow is enough.
|
|
865
|
+
- You cannot author the wireframe without making up details the spec does not support — put those in `diagramModel.openQuestions` instead.
|
|
866
|
+
|
|
867
|
+
---
|
|
868
|
+
|
|
576
869
|
## 9. diagramModel (mandatory)
|
|
577
870
|
|
|
578
871
|
Define this variable in every generated HTML `<script>` block, following the `model-shape.md` spec for `kind: 'use-case'`. Every actor, use case, and relationship must be represented. The Visual Paradigm exporter walks this variable.
|
|
579
872
|
|
|
873
|
+
**v2 note:** each `kind: 'useCase'` element now carries the full documentation (`primaryActor`, `preconditions`, `postconditions`, `scenarios`, `userStories`, optional `wireframe`). `useCaseDocs` in §8a is the in-HTML runtime index keyed by UC id — it can be derived from `diagramModel.parts[*].elements.filter(e => e.kind === 'useCase')` during initialization, or emitted as a pre-built literal. Either way, each UC entry must satisfy the §4.4 gate.
|
|
874
|
+
|
|
580
875
|
### Assumptions and open questions
|
|
581
876
|
|
|
582
877
|
Both `diagramModel` and `diagramLayout` carry two parallel string arrays, matching the BPMN skill's shape:
|
|
@@ -626,6 +921,95 @@ The assumptions panel CSS must be included in the HTML `<style>` block:
|
|
|
626
921
|
.assumptions-col li { color: #334155; font-size: 12px; line-height: 1.6; }
|
|
627
922
|
```
|
|
628
923
|
|
|
924
|
+
### v2 — UC modal styling
|
|
925
|
+
|
|
926
|
+
```css
|
|
927
|
+
/* Lock body scroll while the modal is open */
|
|
928
|
+
body.uc-modal-open { overflow: hidden; }
|
|
929
|
+
|
|
930
|
+
.uc-modal { position: fixed; inset: 0; z-index: 2000; display: flex;
|
|
931
|
+
align-items: flex-start; justify-content: center; padding: 40px 20px;
|
|
932
|
+
overflow-y: auto; direction: rtl;
|
|
933
|
+
font-family: 'Noto Sans Hebrew', sans-serif; }
|
|
934
|
+
.uc-modal-backdrop { position: absolute; inset: 0; background: rgba(15, 23, 42, 0.55); }
|
|
935
|
+
.uc-modal-body { position: relative; z-index: 1; width: 100%; max-width: 960px;
|
|
936
|
+
background: #fff; border-radius: 12px; box-shadow: 0 10px 40px rgba(0,0,0,.25);
|
|
937
|
+
padding: 28px 32px 32px; color: #1e293b; }
|
|
938
|
+
.uc-modal-close { position: absolute; top: 10px; left: 14px; background: none;
|
|
939
|
+
border: none; font-size: 24px; color: #64748b; cursor: pointer; }
|
|
940
|
+
.uc-modal-body header { border-bottom: 1px solid #e2e8f0; padding-bottom: 12px;
|
|
941
|
+
margin-bottom: 18px; }
|
|
942
|
+
.uc-modal-body .uc-id { display: inline-block; font-family: 'IBM Plex Mono',monospace;
|
|
943
|
+
font-size: 11px; color: #64748b; background: #f1f5f9; padding: 2px 8px;
|
|
944
|
+
border-radius: 4px; margin-bottom: 6px; }
|
|
945
|
+
.uc-modal-body .uc-name { margin: 0 0 4px; font-size: 20px; color: #1e40af; font-weight: 700; }
|
|
946
|
+
.uc-modal-body .uc-primary { font-size: 13px; color: #334155; }
|
|
947
|
+
.uc-modal-body .uc-primary-label { color: #64748b; margin-left: 6px; }
|
|
948
|
+
.uc-modal-body .uc-primary-link { color: #1e40af; cursor: pointer; font-weight: 600;
|
|
949
|
+
text-decoration: underline dotted; }
|
|
950
|
+
.uc-description { margin-bottom: 20px; font-size: 14px; line-height: 1.7; color: #334155; }
|
|
951
|
+
|
|
952
|
+
.uc-scenarios h3 { font-size: 14px; color: #1e40af; margin: 18px 0 6px;
|
|
953
|
+
border-right: 3px solid #3b82f6; padding-right: 10px; }
|
|
954
|
+
.uc-scenarios ol { margin: 0 0 14px; padding-right: 22px; }
|
|
955
|
+
.uc-scenarios ol li { color: #1e293b; font-size: 13px; line-height: 1.9; }
|
|
956
|
+
.step-actor { color: #1e40af; font-weight: 600; cursor: pointer; text-decoration: underline dotted; }
|
|
957
|
+
.step-system { color: #475569; font-style: italic; }
|
|
958
|
+
.step-include { color: #7c3aed; font-family: 'IBM Plex Mono', monospace; font-size: 11px; }
|
|
959
|
+
.step-uc-ref { color: #7c3aed; cursor: pointer; text-decoration: underline; }
|
|
960
|
+
.ext-inline, .ext-sub { list-style: none; color: #64748b; font-size: 12px;
|
|
961
|
+
padding-right: 24px; line-height: 1.7; }
|
|
962
|
+
|
|
963
|
+
.uc-prepostuser { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 18px;
|
|
964
|
+
margin: 18px 0 20px; }
|
|
965
|
+
.uc-prepostuser > div { background: #f8fafc; border: 1px solid #e2e8f0;
|
|
966
|
+
border-radius: 8px; padding: 12px 14px; }
|
|
967
|
+
.uc-prepostuser h4 { margin: 0 0 6px; font-size: 12px; color: #1e40af; font-weight: 600; }
|
|
968
|
+
.uc-prepostuser ul { margin: 0; padding-right: 18px; font-size: 12px;
|
|
969
|
+
color: #334155; line-height: 1.7; }
|
|
970
|
+
|
|
971
|
+
.uc-wireframe { border-top: 1px dashed #cbd5e1; padding-top: 16px; margin-top: 6px; }
|
|
972
|
+
.uc-wireframe:empty, .uc-wireframe[hidden] { display: none; }
|
|
973
|
+
|
|
974
|
+
/* Actor pulse animation used by pulseActor() */
|
|
975
|
+
.actor-pulse { animation: actor-pulse 0.6s ease-in-out 3; }
|
|
976
|
+
@keyframes actor-pulse {
|
|
977
|
+
0%, 100% { filter: none; }
|
|
978
|
+
50% { filter: drop-shadow(0 0 8px #3b82f6); }
|
|
979
|
+
}
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
### v2 — Wireframe scoped theme
|
|
983
|
+
|
|
984
|
+
```css
|
|
985
|
+
.wf-screen { background: #f8fafc; border: 1px solid #cbd5e1; border-radius: 8px;
|
|
986
|
+
padding: 18px 20px; font-family: 'Noto Sans Hebrew', sans-serif;
|
|
987
|
+
color: #1e293b; direction: rtl; max-width: 760px; margin: 0 auto; }
|
|
988
|
+
.wf-heading { margin: 0 0 14px; font-size: 15px; color: #1e40af; font-weight: 700;
|
|
989
|
+
border-bottom: 1px solid #e2e8f0; padding-bottom: 6px; }
|
|
990
|
+
.wf-section { margin: 14px 0; }
|
|
991
|
+
.wf-row { display: flex; gap: 10px; align-items: center; margin: 6px 0; }
|
|
992
|
+
.wf-label { font-size: 12px; color: #475569; min-width: 120px; }
|
|
993
|
+
.wf-input, .wf-textarea, .wf-select { flex: 1; background: #fff;
|
|
994
|
+
border: 1px solid #cbd5e1; border-radius: 4px; padding: 6px 8px;
|
|
995
|
+
font-size: 12px; color: #334155; }
|
|
996
|
+
.wf-textarea { min-height: 60px; }
|
|
997
|
+
.wf-table { width: 100%; border-collapse: collapse; margin: 10px 0; font-size: 12px; }
|
|
998
|
+
.wf-table th, .wf-table td { border: 1px solid #cbd5e1; padding: 6px 8px;
|
|
999
|
+
text-align: right; }
|
|
1000
|
+
.wf-table th { background: #e2e8f0; color: #1e40af; font-weight: 600; }
|
|
1001
|
+
.wf-actions { display: flex; gap: 8px; margin-top: 12px; justify-content: flex-start; }
|
|
1002
|
+
.wf-button { background: #fff; border: 1px solid #64748b; border-radius: 4px;
|
|
1003
|
+
padding: 6px 14px; font-size: 12px; color: #334155; cursor: default; }
|
|
1004
|
+
.wf-primary { background: #1e40af; color: #fff; border-color: #1e40af; }
|
|
1005
|
+
.wf-image-box { border: 1px dashed #94a3b8; background: #f1f5f9; color: #64748b;
|
|
1006
|
+
text-align: center; padding: 24px; border-radius: 4px; font-size: 12px;
|
|
1007
|
+
margin: 10px 0; }
|
|
1008
|
+
.wf-hint { font-size: 11px; color: #64748b; font-style: italic; margin: 6px 0; }
|
|
1009
|
+
.wf-badge { display: inline-block; background: #e0f2fe; color: #0369a1;
|
|
1010
|
+
padding: 2px 8px; border-radius: 999px; font-size: 10px; font-weight: 600; }
|
|
1011
|
+
```
|
|
1012
|
+
|
|
629
1013
|
---
|
|
630
1014
|
|
|
631
1015
|
## 11. HTML File Structure
|
|
@@ -677,13 +1061,38 @@ The assumptions panel CSS must be included in the HTML `<style>` block:
|
|
|
677
1061
|
</div>
|
|
678
1062
|
</div>
|
|
679
1063
|
|
|
1064
|
+
<!-- v2 — UC modal (populated by openUcModal; hidden by default) -->
|
|
1065
|
+
<div id="uc-modal" class="uc-modal" hidden role="dialog" aria-modal="true">
|
|
1066
|
+
<div class="uc-modal-backdrop" onclick="closeUcModal()"></div>
|
|
1067
|
+
<div class="uc-modal-body">
|
|
1068
|
+
<button class="uc-modal-close" onclick="closeUcModal()" aria-label="Close">×</button>
|
|
1069
|
+
<header>
|
|
1070
|
+
<span class="uc-id"></span>
|
|
1071
|
+
<h2 class="uc-name"></h2>
|
|
1072
|
+
<div class="uc-primary">
|
|
1073
|
+
<span class="uc-primary-label">שחקן ראשי:</span>
|
|
1074
|
+
<a class="uc-primary-link"></a>
|
|
1075
|
+
</div>
|
|
1076
|
+
</header>
|
|
1077
|
+
<section class="uc-description"></section>
|
|
1078
|
+
<section class="uc-scenarios"></section>
|
|
1079
|
+
<section class="uc-prepostuser">
|
|
1080
|
+
<div class="uc-pre"><h4>תנאים מוקדמים</h4><ul></ul></div>
|
|
1081
|
+
<div class="uc-post"><h4>תנאים סופיים</h4><ul></ul></div>
|
|
1082
|
+
<div class="uc-stories"><h4>סיפורי משתמש</h4><ul></ul></div>
|
|
1083
|
+
</section>
|
|
1084
|
+
<section class="uc-wireframe" hidden></section>
|
|
1085
|
+
</div>
|
|
1086
|
+
</div>
|
|
1087
|
+
|
|
680
1088
|
<!-- VP export button -->
|
|
681
1089
|
<button class="vp-export-btn" onclick="exportXMI()">Download .xmi (Visual Paradigm)</button>
|
|
682
1090
|
|
|
683
1091
|
<script>
|
|
684
1092
|
const diagramLayouts = [ /* one layout object per tab, per §6 */ ];
|
|
685
|
-
const
|
|
686
|
-
const
|
|
1093
|
+
const actorDescriptions = { /* §8b */ };
|
|
1094
|
+
const assocDescriptions = { /* §8b */ };
|
|
1095
|
+
const useCaseDocs = { /* §8a — full v2 per-UC documentation */ };
|
|
687
1096
|
const diagramModel = { /* model-shape.md, kind: 'use-case' */ };
|
|
688
1097
|
|
|
689
1098
|
let activeTab = 0;
|
|
@@ -695,11 +1104,16 @@ The assumptions panel CSS must be included in the HTML `<style>` block:
|
|
|
695
1104
|
fixLayout(diagramLayouts[index]); // idempotent
|
|
696
1105
|
}
|
|
697
1106
|
|
|
698
|
-
function fixLayout(layout)
|
|
699
|
-
function showDesc(id, type, event)
|
|
700
|
-
function
|
|
701
|
-
function
|
|
702
|
-
function
|
|
1107
|
+
function fixLayout(layout) { /* §6 */ }
|
|
1108
|
+
function showDesc(id, type, event){ /* §8: routes to openUcModal for UCs, showPopup otherwise */ }
|
|
1109
|
+
function showPopup(id, type, event){ /* §8b */ }
|
|
1110
|
+
function hideDesc() { /* §8b */ }
|
|
1111
|
+
function openUcModal(id) { /* §8a */ }
|
|
1112
|
+
function closeUcModal() { /* §8a */ }
|
|
1113
|
+
function pulseActor(actorId) { /* §8a */ }
|
|
1114
|
+
function wireframeSafe(html) { /* §8a / §WF */ }
|
|
1115
|
+
function xmiBody(elements, rels) { /* export-buttons.md — v2 emits preconditions/postconditions/ownedBehavior */ }
|
|
1116
|
+
function exportXMI() { /* export-buttons.md */ }
|
|
703
1117
|
|
|
704
1118
|
function renderAssumptions() {
|
|
705
1119
|
const a = (diagramModel && diagramModel.assumptions) || [];
|
|
@@ -738,13 +1152,43 @@ The assumptions panel CSS must be included in the HTML `<style>` block:
|
|
|
738
1152
|
|
|
739
1153
|
## 12. Output
|
|
740
1154
|
|
|
741
|
-
|
|
742
|
-
|
|
1155
|
+
**Mandatory validator step — call before writing the HTML.**
|
|
1156
|
+
|
|
1157
|
+
After assembling `diagramModel` and before generating any HTML, serialize it to JSON and call the MCP tool `uml_usecase_validate_model`:
|
|
1158
|
+
|
|
1159
|
+
```
|
|
1160
|
+
uml_usecase_validate_model(model_json = JSON.stringify(diagramModel))
|
|
1161
|
+
```
|
|
1162
|
+
|
|
1163
|
+
The validator runs locally (no API cost) and catches what the §4 and CR gates cannot enforce from prose:
|
|
1164
|
+
- Include target with an actor association (CR #4) → ERROR
|
|
1165
|
+
- Single-incoming «include» (CR #4) → ERROR
|
|
1166
|
+
- Send-only UC names like `Print Shipping Label` / `שליחת X` (CR #10) → ERROR
|
|
1167
|
+
- Physical-verb UC names (`Perform X`, `ביצוע X`) not wrapped with a system verb (CR #8) → WARNING
|
|
1168
|
+
- Primary actor missing, unknown, or not connected as initiator (§4.1/§4.4) → ERROR
|
|
1169
|
+
- Flow step references to unknown actor/UC ids, or `«include»` steps without a matching `include` relationship → ERROR
|
|
1170
|
+
- Extensions referencing invalid step numbers → ERROR
|
|
1171
|
+
- Narrative text missing Hebrew characters (description, preconditions, postconditions, flow/extension text, user stories, assumptions, openQuestions) → ERROR
|
|
1172
|
+
- Unsafe wireframe HTML (script / on*= / iframe / external URL / @import / inline style) → ERROR
|
|
1173
|
+
- Warnings: fewer than 2 user stories on a UC, no preconditions/postconditions, overall >2 include+extend relationships
|
|
1174
|
+
|
|
1175
|
+
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`.**
|
|
1176
|
+
|
|
1177
|
+
After the validator passes, save the HTML as `[system_name]_use_case_diagram.html`.
|
|
1178
|
+
Briefly report: actor count, use case count, include/extend count, whether split, and any warnings the validator surfaced.
|
|
743
1179
|
|
|
744
1180
|
---
|
|
745
1181
|
|
|
746
1182
|
## 13. Pre-Delivery Checklist
|
|
747
1183
|
|
|
1184
|
+
- [ ] `uml_usecase_validate_model` was called and returned `✓ Model valid` on the final model (§12). Warnings may remain but every ERROR is fixed.
|
|
1185
|
+
- [ ] 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.
|
|
1186
|
+
- [ ] 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.
|
|
1187
|
+
- [ ] 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`.
|
|
1188
|
+
- [ ] Every `primaryActor` id exists in `actors[]` AND has an association with `role: 'initiator'` on that UC (aligning §4.1 with §4.4).
|
|
1189
|
+
- [ ] 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.
|
|
1190
|
+
- [ ] 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.
|
|
1191
|
+
- [ ] Any `wireframe` HTML passes `wireframeSafe()` (no `<script>`, no `on*=`, no `<iframe>`, no external URLs, no `@import`, no inline `<style>`); all classes are `wf-*` only.
|
|
748
1192
|
- [ ] Every actor-UC association has a written §4.1 justification block (role, initiates? one-line "why"); every UC has ≥1 association with `initiates = yes`.
|
|
749
1193
|
- [ ] 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.
|
|
750
1194
|
- [ ] `assumptions` and `openQuestions` arrays are present in `diagramModel`; the HTML renders the bottom panel whenever either is non-empty.
|