@miragon/design-iq-validator 5.3.0 → 5.5.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/README.md CHANGED
@@ -5,7 +5,9 @@ structural checks (flow soundness, complete BPMNDI coverage), DMN structure and
5
5
  the generic cross-model reference rule (every required reference — `callActivity` calls,
6
6
  `businessRuleTask` decides — must resolve in the repo), and a per-mediaKind parse baseline for
7
7
  every other registered notation (a broken `.tt`/`.vc.json` is an ERROR; DSL notations are
8
- lenient by design). Every finding carries a stable `ruleId` (e.g. `bpmn/flow`, `dmn/di`,
8
+ lenient by design; a `mermaid` fence in a Markdown document that does not start with a diagram
9
+ type such as `flowchart` or `sequenceDiagram` is a WARN, `mermaid/diagram-type`, naming the
10
+ document's line). Every finding carries a stable `ruleId` (e.g. `bpmn/flow`, `dmn/di`,
9
11
  `refs/dangling`). It treats the target repo as pure data — it never executes content-repo code.
10
12
  Exit code 0 = no errors (warnings allowed), 1 = errors.
11
13
 
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as runCli } from "./validate-D5Lo0E8X.js";
2
+ import { a as runCli } from "./validate-DjtZJTGk.js";
3
3
  //#region src/cli.ts
4
4
  /**
5
5
  * CLI entry for the platform validator. A dedicated entry file (instead of an
@@ -0,0 +1 @@
1
+ export { t as CONTENT_CONFIG_FILE, i as contentConfigConflict, a as discoverModels, o as legacyContentConfigFile, s as loadContentConfig } from "./content-Bsv9ZLFd.js";
@@ -84,6 +84,457 @@ const notContentRepoError = (root) => {
84
84
  return found === void 0 ? `[ERROR] ${root}: no ${CONTENT_CONFIG_NAMES} at the root — not a content repo (or wrong --root)` : `[ERROR] ${root}: ${found} at the root names no models folder — not a content repo (or wrong --root)`;
85
85
  };
86
86
  //#endregion
87
+ //#region ../notations/markdown.ts
88
+ const NONE = { kind: "none" };
89
+ const PARAGRAPH = { kind: "paragraph" };
90
+ const CODE = { kind: "code" };
91
+ const FENCE_OPEN = /^(`{3,}|~{3,})(.*)$/;
92
+ const THEMATIC_BREAK = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/;
93
+ const ATX_HEADING = /^#{1,6}(?:[ \t]|$)/;
94
+ const SETEXT_UNDERLINE = /^(?:=+|-+)[ \t]*$/;
95
+ const BULLET = /^[-+*](?=[ \t]|$)/;
96
+ const ORDERED = /^(\d{1,9})[.)](?=[ \t]|$)/;
97
+ const BLANK = /^[ \t]*$/;
98
+ /** a link reference definition on one line: [label]: destination "title" (a
99
+ * "[^" label is a footnote, which render.ts keeps as paragraph text; a bare
100
+ * destination ends at a space or a control character, as in markdown-it) */
101
+ const LINK_DEFINITION = /^\[(?!\^)((?:[^[\]\\]|\\.)*)\]:[ \t]*(<(?:[^<>\\]|\\.)*>|[^ <\x00-\x1f\x7f][^ \x00-\x1f\x7f]*)(?:[ \t]+(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\((?:[^()\\]|\\.)*\)))?[ \t]*$/;
102
+ /** the destinations markdown-it's validateLink refuses: no definition then */
103
+ const BAD_LINK = /^(?:vbscript|javascript|file|data):/i;
104
+ const GOOD_DATA_LINK = /^data:image\/(?:gif|png|jpeg|webp);/i;
105
+ /** markdown-it's maxNesting: content nested this deep (a Container's `level`)
106
+ * it does not parse, and drops the rest of the innermost quote around it,
107
+ * or of the document */
108
+ const MAX_NESTING = 100;
109
+ /** start, end and whether it may interrupt a paragraph; an end of /^$/ is the next blank line */
110
+ const HTML_BLOCKS = [
111
+ [
112
+ /^<(?:script|pre|style|textarea)(?=\s|>|$)/i,
113
+ /<\/(?:script|pre|style|textarea)>/i,
114
+ true
115
+ ],
116
+ [
117
+ /^<!--/,
118
+ /-->/,
119
+ true
120
+ ],
121
+ [
122
+ /^<\?/,
123
+ /\?>/,
124
+ true
125
+ ],
126
+ [
127
+ /^<![A-Za-z]/,
128
+ />/,
129
+ true
130
+ ],
131
+ [
132
+ /^<!\[CDATA\[/,
133
+ /\]\]>/,
134
+ true
135
+ ],
136
+ [
137
+ new RegExp(`^</?(?:address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h1|h2|h3|h4|h5|h6|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul)(?=\\s|/?>|$)`, "i"),
138
+ /^$/,
139
+ true
140
+ ],
141
+ [
142
+ new RegExp(`^(?:<[A-Za-z][A-Za-z0-9\\-]*(?:\\s+[a-zA-Z_:][a-zA-Z0-9:._-]*(?:\\s*=\\s*(?:[^"'=<>\`\\x00-\\x20]+|'[^']*'|"[^"]*"))?)*\\s*\\/?>|<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>)\\s*$`),
143
+ /^$/,
144
+ false
145
+ ]
146
+ ];
147
+ /** the leading blanks of a rest */
148
+ function leading(rest) {
149
+ let col = rest.col;
150
+ let i = 0;
151
+ for (; i < rest.text.length; i++) {
152
+ const ch = rest.text[i];
153
+ if (ch === " ") col++;
154
+ else if (ch === " ") col += 4 - col % 4;
155
+ else break;
156
+ }
157
+ return {
158
+ indent: col - rest.col,
159
+ blank: i === rest.text.length
160
+ };
161
+ }
162
+ /** the columns of leading blanks */
163
+ const indentOf = (rest) => leading(rest).indent;
164
+ /** drop up to `n` columns of leading blanks; a tab cut in two leaves its other columns as spaces */
165
+ function dropIndent(rest, n) {
166
+ const target = rest.col + n;
167
+ let col = rest.col;
168
+ let i = 0;
169
+ while (i < rest.text.length && col < target) {
170
+ const ch = rest.text[i];
171
+ if (ch === " ") col++;
172
+ else if (ch === " ") {
173
+ const next = col + 4 - col % 4;
174
+ if (next > target) return {
175
+ text: " ".repeat(next - target) + rest.text.slice(i + 1),
176
+ col: target
177
+ };
178
+ col = next;
179
+ } else break;
180
+ i++;
181
+ }
182
+ return {
183
+ text: rest.text.slice(i),
184
+ col
185
+ };
186
+ }
187
+ /** the line after its indent, when that is at most three columns (more is indented code) */
188
+ function blockStart(rest) {
189
+ const indent = indentOf(rest);
190
+ return indent < 4 ? dropIndent(rest, indent) : void 0;
191
+ }
192
+ /** a quote's ">" and the one column of blank after it: a space, or the first
193
+ * column of a tab (which stays, a column narrower, as in markdown-it) */
194
+ function afterQuoteMarker(at) {
195
+ const rest = {
196
+ text: at.text.slice(1),
197
+ col: at.col + 1
198
+ };
199
+ if (rest.text.startsWith(" ") || rest.text.startsWith(" ") && rest.col % 4 === 3) return {
200
+ text: rest.text.slice(1),
201
+ col: rest.col + 1
202
+ };
203
+ return rest.text.startsWith(" ") ? {
204
+ text: rest.text,
205
+ col: rest.col + 1
206
+ } : rest;
207
+ }
208
+ /** the rest of the line when it continues the container, undefined when the
209
+ * container ends. `lead` is the rest's leading blanks, kept up to date across
210
+ * the columns a list item drops, so a line is not measured once per container */
211
+ function continues(container, rest, lead) {
212
+ if (container.kind === "quote") {
213
+ const at = dropIndent(rest, lead.indent);
214
+ return at.text.startsWith(">") ? afterQuoteMarker(at) : void 0;
215
+ }
216
+ const fresh = container.fresh;
217
+ container.fresh = false;
218
+ if (lead.blank ? fresh : lead.indent < container.indent) return void 0;
219
+ lead.indent = Math.max(0, lead.indent - container.indent);
220
+ return dropIndent(rest, container.indent);
221
+ }
222
+ /** a list item opening at `at` (the line after its indent of `indent` columns),
223
+ * inside containers nested `level` deep */
224
+ function listItem(at, indent, level, interruptsParagraph) {
225
+ const ordered = ORDERED.exec(at.text);
226
+ const marker = ordered ?? BULLET.exec(at.text);
227
+ if (!marker) return void 0;
228
+ const after = {
229
+ text: at.text.slice(marker[0].length),
230
+ col: at.col + marker[0].length
231
+ };
232
+ const empty = BLANK.test(after.text);
233
+ if (interruptsParagraph && (empty || ordered && Number(ordered[1]) !== 1)) return void 0;
234
+ const blanks = indentOf(after);
235
+ const gap = empty || blanks > 4 ? 1 : blanks;
236
+ return {
237
+ item: {
238
+ kind: "item",
239
+ level: level + 2,
240
+ indent: indent + marker[0].length + gap,
241
+ fresh: empty
242
+ },
243
+ rest: dropIndent(after, gap)
244
+ };
245
+ }
246
+ /** the HTML block starting at `text` (a line after its indent), if any */
247
+ function htmlBlock(text) {
248
+ return text.startsWith("<") ? HTML_BLOCKS.find(([start]) => start.test(text)) : void 0;
249
+ }
250
+ /** whether a line that does not continue a paragraph's containers starts a
251
+ * block of its own (else it is a lazy continuation line of the paragraph) */
252
+ function startsBlock(rest) {
253
+ const at = blockStart(rest);
254
+ if (!at) return false;
255
+ const text = at.text;
256
+ return text.startsWith(">") || fenceOpen(text) !== void 0 || THEMATIC_BREAK.test(text) || BULLET.test(text) || ORDERED.test(text) || ATX_HEADING.test(text) || htmlBlock(text)?.[2] === true;
257
+ }
258
+ /** whether `text` (a line after its indent) is a link reference definition that
259
+ * fits on it and that markdown-it takes: a label with more than blanks, a
260
+ * destination with balanced parentheses and a scheme its validateLink allows */
261
+ function linkDefinition(text) {
262
+ const definition = LINK_DEFINITION.exec(text);
263
+ if (!definition || definition[1].trim() === "") return false;
264
+ let destination = definition[2];
265
+ if (destination.startsWith("<")) destination = destination.slice(1, -1);
266
+ else {
267
+ let depth = 0;
268
+ for (let i = 0; i < destination.length && depth >= 0; i++) {
269
+ const ch = destination[i];
270
+ if (ch === "\\") i++;
271
+ else if (ch === "(") depth = depth < 32 ? depth + 1 : -1;
272
+ else if (ch === ")") depth--;
273
+ }
274
+ if (depth !== 0) return false;
275
+ }
276
+ destination = destination.trim();
277
+ return !BAD_LINK.test(destination) || GOOD_DATA_LINK.test(destination);
278
+ }
279
+ /** the opening run of a fence at `text` (a line after its indent), if any */
280
+ function fenceOpen(text) {
281
+ const open = FENCE_OPEN.exec(text);
282
+ if (!open) return void 0;
283
+ const run = open[1];
284
+ const info = open[2];
285
+ if (run.startsWith("`") && info.includes("`")) return void 0;
286
+ return {
287
+ char: run[0],
288
+ size: run.length,
289
+ info: info.trim()
290
+ };
291
+ }
292
+ /** whether the line is the closing run of `fence` */
293
+ function closes(rest, fence) {
294
+ const at = blockStart(rest);
295
+ if (!at) return false;
296
+ let n = 0;
297
+ while (at.text[n] === fence.char) n++;
298
+ return n >= fence.size && BLANK.test(at.text.slice(n));
299
+ }
300
+ /** every fenced code block of a Markdown document, in document order */
301
+ function markdownFences(source) {
302
+ const lines = source.split(/\r\n|\r|\n/);
303
+ const brokenLast = BLANK.test(lines[lines.length - 1]);
304
+ if (brokenLast) lines.pop();
305
+ const fences = [];
306
+ const stack = [];
307
+ const level = () => stack.length > 0 ? stack[stack.length - 1].level : 0;
308
+ let leaf = NONE;
309
+ let dropping;
310
+ const finish = (fence, closed) => {
311
+ fences.push({
312
+ line: fence.line,
313
+ endLine: fence.line + fence.content.length + (closed ? 1 : 0),
314
+ info: fence.info,
315
+ lang: (fence.info.split(/\s+/)[0] ?? "").toLowerCase(),
316
+ content: fence.content.join(""),
317
+ closed
318
+ });
319
+ };
320
+ for (let i = 0; i < lines.length; i++) {
321
+ let rest = {
322
+ text: lines[i],
323
+ col: 0
324
+ };
325
+ const lineBreak = i < lines.length - 1 || brokenLast ? "\n" : "";
326
+ let matched = 0;
327
+ let lead;
328
+ for (const container of stack) {
329
+ const next = continues(container, rest, lead ??= leading(rest));
330
+ if (!next) break;
331
+ rest = next;
332
+ if (container.kind === "quote") lead = void 0;
333
+ matched++;
334
+ }
335
+ const all = matched === stack.length;
336
+ if (dropping) {
337
+ if (all) {
338
+ dropping.lastBlank = BLANK.test(rest.text);
339
+ continue;
340
+ }
341
+ if (!dropping.lastBlank && !BLANK.test(rest.text) && !startsBlock(rest)) continue;
342
+ dropping = void 0;
343
+ }
344
+ if (leaf.kind === "fence" && all) {
345
+ if (closes(rest, leaf)) {
346
+ finish(leaf, true);
347
+ leaf = NONE;
348
+ } else leaf.content.push(dropIndent(rest, leaf.indent).text + lineBreak);
349
+ continue;
350
+ }
351
+ if (leaf.kind === "html" && all) {
352
+ if (leaf.end.test(dropIndent(rest, indentOf(rest)).text)) leaf = NONE;
353
+ continue;
354
+ }
355
+ if (!all) {
356
+ if (leaf.kind === "paragraph" && !BLANK.test(rest.text) && !startsBlock(rest)) continue;
357
+ if (leaf.kind === "fence") finish(leaf, false);
358
+ stack.length = matched;
359
+ leaf = NONE;
360
+ }
361
+ while (level() < MAX_NESTING && !BLANK.test(rest.text)) {
362
+ const at = blockStart(rest);
363
+ if (!at) break;
364
+ if (at.text.startsWith(">")) {
365
+ stack.push({
366
+ kind: "quote",
367
+ level: level() + 1
368
+ });
369
+ rest = afterQuoteMarker(at);
370
+ } else {
371
+ if (THEMATIC_BREAK.test(at.text)) break;
372
+ const opening = listItem(at, indentOf(rest), level(), leaf.kind === "paragraph");
373
+ if (!opening) break;
374
+ stack.push(opening.item);
375
+ rest = opening.rest;
376
+ }
377
+ leaf = NONE;
378
+ }
379
+ if (BLANK.test(rest.text)) {
380
+ if (leaf.kind === "paragraph") leaf = NONE;
381
+ continue;
382
+ }
383
+ if (level() >= MAX_NESTING) {
384
+ const quote = stack.findLastIndex((container) => container.kind === "quote");
385
+ if (quote < 0) break;
386
+ stack.length = quote + 1;
387
+ leaf = NONE;
388
+ dropping = { lastBlank: false };
389
+ continue;
390
+ }
391
+ const at = blockStart(rest);
392
+ if (!at) {
393
+ if (leaf.kind !== "paragraph") leaf = CODE;
394
+ continue;
395
+ }
396
+ const open = fenceOpen(at.text);
397
+ if (open) {
398
+ leaf = {
399
+ kind: "fence",
400
+ ...open,
401
+ indent: indentOf(rest),
402
+ line: i + 1,
403
+ content: []
404
+ };
405
+ continue;
406
+ }
407
+ const html = htmlBlock(at.text);
408
+ if (html && (html[2] || leaf.kind !== "paragraph")) {
409
+ leaf = html[1].test(at.text) ? NONE : {
410
+ kind: "html",
411
+ end: html[1]
412
+ };
413
+ continue;
414
+ }
415
+ if (leaf.kind !== "paragraph" && linkDefinition(at.text)) leaf = NONE;
416
+ else if (ATX_HEADING.test(at.text) || THEMATIC_BREAK.test(at.text)) leaf = NONE;
417
+ else if (leaf.kind === "paragraph" && SETEXT_UNDERLINE.test(at.text)) leaf = NONE;
418
+ else leaf = PARAGRAPH;
419
+ }
420
+ if (leaf.kind === "fence") finish(leaf, false);
421
+ return fences;
422
+ }
423
+ //#endregion
424
+ //#region ../notations/mermaid.ts
425
+ /**
426
+ * Every diagram type MERMAID_VERSION renders, spelled as its header keyword
427
+ * (its diagram detectors, minus the spellings the detector matches but the
428
+ * grammar then refuses: a bare `architecture` or `requirement`). zenuml is
429
+ * an external plugin mermaid does not bundle, so it is not here.
430
+ */
431
+ const MERMAID_DIAGRAM_TYPES = [
432
+ "flowchart",
433
+ "flowchart-elk",
434
+ "graph",
435
+ "sequenceDiagram",
436
+ "classDiagram",
437
+ "classDiagram-v2",
438
+ "stateDiagram",
439
+ "stateDiagram-v2",
440
+ "erDiagram",
441
+ "journey",
442
+ "gantt",
443
+ "pie",
444
+ "quadrantChart",
445
+ "requirementDiagram",
446
+ "gitGraph",
447
+ "C4Context",
448
+ "C4Container",
449
+ "C4Component",
450
+ "C4Dynamic",
451
+ "C4Deployment",
452
+ "mindmap",
453
+ "timeline",
454
+ "sankey",
455
+ "sankey-beta",
456
+ "xychart",
457
+ "xychart-beta",
458
+ "block",
459
+ "block-beta",
460
+ "packet",
461
+ "packet-beta",
462
+ "kanban",
463
+ "architecture-beta",
464
+ "radar-beta",
465
+ "treemap",
466
+ "treemap-beta",
467
+ "info",
468
+ "swimlane-beta",
469
+ "treeView-beta",
470
+ "eventmodeling",
471
+ "ishikawa",
472
+ "ishikawa-beta",
473
+ "venn-beta",
474
+ "wardley-beta",
475
+ "cynefin-beta",
476
+ "railroad-beta",
477
+ "railroad-ebnf-beta",
478
+ "railroad-abnf-beta",
479
+ "railroad-peg-beta"
480
+ ];
481
+ const FRONT_MATTER = /^([^\S\n\r]*)-{3}\s*[\n\r](.*?)[\n\r]\1-{3}\s*[\n\r]+/s;
482
+ const DIRECTIVE = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
483
+ const COMMENT_LINE = /^\s*%%(?!{)[^\n]+\n?/gm;
484
+ /** a header keyword, followed by the end of the line, a space, ":" or ";" */
485
+ const HEADER = new RegExp(`^(?:${MERMAID_DIAGRAM_TYPES.map((t) => t.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&")).join("|")})(?=$|[\\s:;])`);
486
+ /** mermaid's cleanupText: line breaks to \n, tag attributes single-quoted;
487
+ * the line structure stays, so the source lines can be matched against it */
488
+ function cleanup(source) {
489
+ return source.replace(/\r\n?/g, "\n").replace(/<(\w+)([^>]*)>/g, (_tag, name, attrs) => `<${name}${attrs.replace(/="([^"]*)"/g, "='$1'")}>`);
490
+ }
491
+ /** The text mermaid's diagram parser reads: the source after mermaid's
492
+ * preprocess (front matter, directives and comment lines removed, leading
493
+ * whitespace trimmed). */
494
+ function mermaidCode(source) {
495
+ return cleanup(source).replace(FRONT_MATTER, "").replace(DIRECTIVE, "").replace(COMMENT_LINE, "").trimStart();
496
+ }
497
+ /**
498
+ * The 1-based source line of the 1-based line `codeLine` of mermaidCode(source).
499
+ * The preprocess only removes lines (and joins the two ends of a directive
500
+ * that spans lines), so the code is the source's lines in order: each code
501
+ * line is matched to the next source line with the same text. A line the
502
+ * preprocess rewrote matches nothing and takes the next line; a line past the
503
+ * end clamps to the last one.
504
+ */
505
+ function mermaidSourceLine(source, codeLine) {
506
+ const cleaned = cleanup(source);
507
+ const sourceLines = cleaned.split("\n").map((l) => l.trim());
508
+ const codeLines = mermaidCode(source).split("\n");
509
+ let next = (cleaned.match(FRONT_MATTER)?.[0] ?? "").split("\n").length - 1;
510
+ let at = next;
511
+ for (let i = 0; i < Math.min(codeLine, codeLines.length); i++) {
512
+ const text = codeLines[i].trim();
513
+ let k = next;
514
+ while (k < sourceLines.length && sourceLines[k] !== text) k++;
515
+ at = k < sourceLines.length ? k : next;
516
+ next = at + 1;
517
+ }
518
+ return Math.min(at, sourceLines.length - 1) + 1;
519
+ }
520
+ /**
521
+ * The line that names the diagram type: the first line of mermaidCode that
522
+ * is neither blank nor a bare `%%` comment (mermaid's type detection skips
523
+ * those too). undefined when there is none: the source is empty, or nothing
524
+ * but front matter, directives and comments.
525
+ */
526
+ function mermaidHeader(source) {
527
+ const lines = mermaidCode(source).split("\n");
528
+ const index = lines.findIndex((l) => l.trim() !== "" && !l.trim().startsWith("%%"));
529
+ if (index < 0) return void 0;
530
+ const text = lines[index].trim();
531
+ return {
532
+ line: mermaidSourceLine(source, index + 1),
533
+ text,
534
+ diagramType: HEADER.exec(text)?.[0]
535
+ };
536
+ }
537
+ //#endregion
87
538
  //#region src/validate.ts
88
539
  /**
89
540
  * Deterministic validation of a content repository (the slim contract).
@@ -335,6 +786,24 @@ function checkDmnXml(raw, opts = {}) {
335
786
  else for (const id of diRequired) if (!di.has(id)) err("dmn/di", `${id} has no DMNDI shape (breaks the visual editor)`);
336
787
  return { findings };
337
788
  }
789
+ /** the Mermaid types the header message names: the common ones, not every one */
790
+ const COMMON_MERMAID_TYPES = "flowchart, sequenceDiagram, classDiagram, stateDiagram-v2, erDiagram, gantt, pie, mindmap";
791
+ /**
792
+ * Mermaid's own first step (its type detection, @designiq/notations/mermaid
793
+ * mirrors it) for a ```mermaid fence opening on document line `line`: after
794
+ * the front matter, directives and comments, the diagram's first line must
795
+ * start with a diagram type. Light on purpose: the grammar itself needs
796
+ * mermaid and a DOM, which the validator never loads; the Markdown view
797
+ * shows those errors live. The message names the document's line (a Finding
798
+ * has no line of its own).
799
+ */
800
+ function mermaidFenceProblem(content, line) {
801
+ const header = mermaidHeader(content);
802
+ if (!header) return `line ${line}: empty Mermaid diagram: the first line must name a diagram type (${COMMON_MERMAID_TYPES}, …)`;
803
+ if (header.diagramType) return void 0;
804
+ const word = header.text.split(/[\s:;]/, 1)[0];
805
+ return `line ${line + header.line}: '${word}' is not a Mermaid diagram type (${COMMON_MERMAID_TYPES}, …)`;
806
+ }
338
807
  /**
339
808
  * Baseline parse gate for models WITHOUT a dedicated checker (any registered
340
809
  * notation that is not .bpmn/.dmn): a discovered model must at minimum parse
@@ -368,10 +837,21 @@ function checkModelBaseline(raw, opts) {
368
837
  if (wf !== true) err(`not well-formed XML: ${wf.err.msg}`);
369
838
  break;
370
839
  }
371
- case "dsl": try {
372
- extractModelGraph(opts.notation, raw);
373
- } catch (e) {
374
- err(`not parseable: ${e.message}`);
840
+ case "dsl":
841
+ try {
842
+ extractModelGraph(opts.notation, raw);
843
+ } catch (e) {
844
+ err(`not parseable: ${e.message}`);
845
+ }
846
+ break;
847
+ case "markdown": for (const fence of markdownFences(raw)) {
848
+ const problem = fence.lang === "mermaid" ? mermaidFenceProblem(fence.content, fence.line) : void 0;
849
+ if (problem) findings.push({
850
+ severity: "WARN",
851
+ ruleId: "mermaid/diagram-type",
852
+ file,
853
+ message: problem
854
+ });
375
855
  }
376
856
  }
377
857
  return findings;
@@ -436,7 +916,7 @@ async function runCli() {
436
916
  /** optional single-model filter (a model file stem) */
437
917
  const only = argv[0];
438
918
  const rel = (p) => p.startsWith("/") ? relative(ROOT, p) : p;
439
- const { CONTENT_CONFIG_FILE, contentConfigConflict, discoverModels, legacyContentConfigFile, loadContentConfig } = await import("./content-DgyuLYdD.js");
919
+ const { CONTENT_CONFIG_FILE, contentConfigConflict, discoverModels, legacyContentConfigFile, loadContentConfig } = await import("./content-wjC_epKd.js");
440
920
  const cfg = loadContentConfig(ROOT);
441
921
  if (!cfg) {
442
922
  console.error(notContentRepoError(ROOT));
package/dist/validate.js CHANGED
@@ -1,2 +1 @@
1
- import { a as runCli, i as checkModelBaseline, n as checkDmnXml, r as checkModel, t as checkBpmnXml } from "./validate-D5Lo0E8X.js";
2
- export { checkBpmnXml, checkDmnXml, checkModel, checkModelBaseline, runCli };
1
+ export { t as checkBpmnXml, n as checkDmnXml, r as checkModel, i as checkModelBaseline, a as runCli } from "./validate-DjtZJTGk.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miragon/design-iq-validator",
3
- "version": "5.3.0",
3
+ "version": "5.5.0",
4
4
  "type": "module",
5
5
  "description": "Platform-owned validator for designIQ content repos — designiq.yml discovery, BPMN/DMN structure and diagram coverage, cross-model references, and a parse check for every other notation. Runs against any checkout via --root; never executes content-repo code.",
6
6
  "license": "MIT",
@@ -38,7 +38,7 @@
38
38
  "yaml": "2.9.1"
39
39
  },
40
40
  "devDependencies": {
41
- "tsdown": "0.23.0",
41
+ "tsdown": "0.23.1",
42
42
  "@designiq/notations": "0.1.0"
43
43
  },
44
44
  "scripts": {
@@ -1,2 +0,0 @@
1
- import { a as discoverModels, i as contentConfigConflict, o as legacyContentConfigFile, s as loadContentConfig, t as CONTENT_CONFIG_FILE } from "./content-Bsv9ZLFd.js";
2
- export { CONTENT_CONFIG_FILE, contentConfigConflict, discoverModels, legacyContentConfigFile, loadContentConfig };