@miragon/design-iq-validator 5.10.0 → 5.11.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
@@ -7,7 +7,8 @@ the generic cross-model reference rule (every required reference — `callActivi
7
7
  notations, and a per-mediaKind parse baseline for every other registered notation (a file that
8
8
  is no JSON or XML is an ERROR; DSL notations are lenient by design; a `mermaid` fence in a
9
9
  Markdown document that does not start with a diagram type such as `flowchart` or
10
- `sequenceDiagram` is a WARN, `mermaid/diagram-type`, naming the document's line). Every finding
10
+ `sequenceDiagram` is a WARN, `mermaid/diagram-type`, naming the document's line; another casing
11
+ passes only where mermaid draws it, so `Ishikawa` passes and `Flowchart` warns). Every finding
11
12
  carries a stable `ruleId` (e.g. `bpmn/flow`, `dmn/di`, `refs/dangling`). It treats the target
12
13
  repo as pure data — it never executes content-repo code. Exit code 0 = no errors (warnings
13
14
  allowed), 1 = errors.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { o as runCli } from "./validate-BWa99fy6.js";
2
+ import { o as runCli } from "./validate-HHrcxggb.js";
3
3
  //#region src/cli.ts
4
4
  /**
5
5
  * CLI entry for the platform validator. A dedicated entry file (instead of an
@@ -545,6 +545,8 @@ const TT_EDGE_INTERACTIONS_VERSION = 1;
545
545
  function extractTeamTopology(raw) {
546
546
  const data = JSON.parse(raw);
547
547
  const list = (v) => Array.isArray(v) ? v : [];
548
+ const objects = (v) => list(v).filter((i) => i !== null && typeof i === "object");
549
+ const text = (v) => typeof v === "string" && v !== "" ? v : void 0;
548
550
  const teams = list(data.nodes).map((n) => ({
549
551
  id: n.id,
550
552
  type: n.type ?? "team",
@@ -552,16 +554,38 @@ function extractTeamTopology(raw) {
552
554
  extra: n.description ? { description: n.description } : void 0
553
555
  }));
554
556
  const placed = (i) => "position" in i && "size" in i;
555
- const interactions = list(data.interactions).filter((i) => i !== null && typeof i === "object").filter((i) => data.version !== TT_EDGE_INTERACTIONS_VERSION || placed(i)).map((i) => ({
557
+ const interactions = objects(data.interactions).filter((i) => data.version !== TT_EDGE_INTERACTIONS_VERSION || placed(i)).map((i) => ({
556
558
  id: i.id,
557
559
  type: "interaction",
558
560
  name: i.label,
559
561
  extra: { mode: i.mode }
560
562
  }));
563
+ const flows = objects(data.flows).map((f) => ({
564
+ id: f.id,
565
+ type: "flow",
566
+ name: text(f.label)
567
+ }));
568
+ const attachable = new Set([
569
+ ...teams,
570
+ ...interactions,
571
+ ...flows
572
+ ].map((n) => n.id));
573
+ const annotations = objects(data.annotations).map((a) => ({
574
+ id: a.id,
575
+ type: "annotation",
576
+ name: text(a.text),
577
+ extra: typeof a.attachedTo === "string" && attachable.has(a.attachedTo) ? { attachedTo: a.attachedTo } : void 0
578
+ }));
561
579
  return {
562
580
  notation: "team-topology",
563
- nodes: [...teams, ...interactions],
564
- edges: []
581
+ nodes: [
582
+ ...teams,
583
+ ...interactions,
584
+ ...flows,
585
+ ...annotations
586
+ ],
587
+ edges: [],
588
+ meta: { title: text(data.title) ?? null }
565
589
  };
566
590
  }
567
591
  const STORM_ID_PREFIX = {
@@ -1 +1 @@
1
- export { t as CONTENT_CONFIG_FILE, i as contentConfigConflict, a as discoverModels, o as legacyContentConfigFile, s as loadContentConfig } from "./content-BjTdVfz8.js";
1
+ export { t as CONTENT_CONFIG_FILE, i as contentConfigConflict, a as discoverModels, o as legacyContentConfigFile, s as loadContentConfig } from "./content-2Udu_aBL.js";
@@ -1,4 +1,4 @@
1
- import { _ as byId, c as resolveContentConfigFile, d as extractModelGraph, f as parseXml, g as byExtension, h as kindOf, l as XMLValidator, m as requirementHref, p as readBpmn, r as CONTENT_CONFIG_NAMES, u as asArray } from "./content-BjTdVfz8.js";
1
+ import { _ as byId, c as resolveContentConfigFile, d as extractModelGraph, f as parseXml, g as byExtension, h as kindOf, l as XMLValidator, m as requirementHref, p as readBpmn, r as CONTENT_CONFIG_NAMES, u as asArray } from "./content-2Udu_aBL.js";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { relative, resolve } from "node:path";
4
4
  import { parseDocumentText, serializeDocumentText, validateDocument } from "@miragon/context-maps-schema-model";
@@ -98,9 +98,14 @@ const SETEXT_UNDERLINE = /^(?:=+|-+)[ \t]*$/;
98
98
  const BULLET = /^[-+*](?=[ \t]|$)/;
99
99
  const ORDERED = /^(\d{1,9})[.)](?=[ \t]|$)/;
100
100
  const BLANK = /^[ \t]*$/;
101
+ /** a footnote definition's marker: a label without blanks */
102
+ const FOOTNOTE_DEFINITION = /^\[\^[^\] ]+\]:/;
103
+ /** the columns a footnote definition's content is indented by past the definition */
104
+ const FOOTNOTE_INDENT = 4;
101
105
  /** a link reference definition on one line: [label]: destination "title" (a
102
- * "[^" label is a footnote, which render.ts keeps as paragraph text; a bare
103
- * destination ends at a space or a control character, as in markdown-it) */
106
+ * "[^" label is a footnote definition, or with a blank in it paragraph text
107
+ * to render.ts; a bare destination ends at a space or a control character,
108
+ * as in markdown-it) */
104
109
  const LINK_DEFINITION = /^\[(?!\^)((?:[^[\]\\]|\\.)*)\]:[ \t]*(<(?:[^<>\\]|\\.)*>|[^ <\x00-\x1f\x7f][^ \x00-\x1f\x7f]*)(?:[ \t]+(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\((?:[^()\\]|\\.)*\)))?[ \t]*$/;
105
110
  /** the destinations markdown-it's validateLink refuses: no definition then */
106
111
  const BAD_LINK = /^(?:vbscript|javascript|file|data):/i;
@@ -216,6 +221,11 @@ function continues(container, rest, lead) {
216
221
  const at = dropIndent(rest, lead.indent);
217
222
  return at.text.startsWith(">") ? afterQuoteMarker(at) : void 0;
218
223
  }
224
+ if (container.kind === "footnote") {
225
+ if (!lead.blank && lead.indent < FOOTNOTE_INDENT) return void 0;
226
+ lead.indent = Math.max(0, lead.indent - FOOTNOTE_INDENT);
227
+ return dropIndent(rest, FOOTNOTE_INDENT);
228
+ }
219
229
  const fresh = container.fresh;
220
230
  container.fresh = false;
221
231
  if (lead.blank ? fresh : lead.indent < container.indent) return void 0;
@@ -250,9 +260,25 @@ function listItem(at, indent, level, interruptsParagraph) {
250
260
  function htmlBlock(text) {
251
261
  return text.startsWith("<") ? HTML_BLOCKS.find(([start]) => start.test(text)) : void 0;
252
262
  }
263
+ /**
264
+ * The content of a footnote definition's first line, `at` the line after its
265
+ * indent: the text after the colon. markdown-it-footnote indents it by the
266
+ * blanks after the colon, less `indent` (the columns the definition's content
267
+ * is indented by in the innermost quote) once there are that many.
268
+ */
269
+ function footnoteContent(at, marker, indent) {
270
+ const after = {
271
+ text: at.text.slice(marker.length),
272
+ col: at.col + marker.length
273
+ };
274
+ return indentOf(after) < indent ? after : dropIndent(after, indent);
275
+ }
253
276
  /** whether a line that does not continue a paragraph's containers starts a
254
- * block of its own (else it is a lazy continuation line of the paragraph) */
255
- function startsBlock(rest) {
277
+ * block of its own (else it is a lazy continuation line of the paragraph).
278
+ * A footnote definition does, whatever its indent, but not in a quote it
279
+ * does not continue: a quote reads it as a lazy line */
280
+ function startsBlock(rest, quoted) {
281
+ if (!quoted && FOOTNOTE_DEFINITION.test(dropIndent(rest, indentOf(rest)).text)) return true;
256
282
  const at = blockStart(rest);
257
283
  if (!at) return false;
258
284
  const text = at.text;
@@ -308,6 +334,16 @@ function markdownFences(source) {
308
334
  const fences = [];
309
335
  const stack = [];
310
336
  const level = () => stack.length > 0 ? stack[stack.length - 1].level : 0;
337
+ /** the columns the innermost container's content is indented by, counted
338
+ * from the innermost quote (inside a quote markdown-it counts from its ">") */
339
+ const contentIndent = () => {
340
+ let indent = 0;
341
+ for (let i = stack.length - 1; i >= 0 && stack[i].kind !== "quote"; i--) {
342
+ const container = stack[i];
343
+ indent += container.kind === "item" ? container.indent : FOOTNOTE_INDENT;
344
+ }
345
+ return indent;
346
+ };
311
347
  let leaf = NONE;
312
348
  let dropping;
313
349
  const finish = (fence, closed) => {
@@ -341,7 +377,7 @@ function markdownFences(source) {
341
377
  dropping.lastBlank = BLANK.test(rest.text);
342
378
  continue;
343
379
  }
344
- if (!dropping.lastBlank && !BLANK.test(rest.text) && !startsBlock(rest)) continue;
380
+ if (!dropping.lastBlank && !BLANK.test(rest.text) && !startsBlock(rest, true)) continue;
345
381
  dropping = void 0;
346
382
  }
347
383
  if (leaf.kind === "fence" && all) {
@@ -356,7 +392,8 @@ function markdownFences(source) {
356
392
  continue;
357
393
  }
358
394
  if (!all) {
359
- if (leaf.kind === "paragraph" && !BLANK.test(rest.text) && !startsBlock(rest)) continue;
395
+ const quoted = stack.slice(matched).some((container) => container.kind === "quote");
396
+ if (leaf.kind === "paragraph" && !BLANK.test(rest.text) && !startsBlock(rest, quoted)) continue;
360
397
  if (leaf.kind === "fence") finish(leaf, false);
361
398
  stack.length = matched;
362
399
  leaf = NONE;
@@ -364,12 +401,19 @@ function markdownFences(source) {
364
401
  while (level() < MAX_NESTING && !BLANK.test(rest.text)) {
365
402
  const at = blockStart(rest);
366
403
  if (!at) break;
404
+ const footnote = FOOTNOTE_DEFINITION.exec(at.text)?.[0];
367
405
  if (at.text.startsWith(">")) {
368
406
  stack.push({
369
407
  kind: "quote",
370
408
  level: level() + 1
371
409
  });
372
410
  rest = afterQuoteMarker(at);
411
+ } else if (footnote !== void 0) {
412
+ stack.push({
413
+ kind: "footnote",
414
+ level: level() + 1
415
+ });
416
+ rest = footnoteContent(at, footnote, contentIndent());
373
417
  } else {
374
418
  if (THEMATIC_BREAK.test(at.text)) break;
375
419
  const opening = listItem(at, indentOf(rest), level(), leaf.kind === "paragraph");
@@ -496,8 +540,44 @@ const KEEPS_COMMENTS = /^(?:eventmodeling|agentflow-beta\b)/;
496
540
  /** mermaid's default maxTextSize, which apps/web keeps (a secure key): past
497
541
  * it, mermaid parses even those types without their comment lines */
498
542
  const MAX_TEXT_SIZE = 5e4;
499
- /** a header keyword, followed by the end of the line, a space, ":" or ";" */
500
- const HEADER = new RegExp(`^(?:${MERMAID_DIAGRAM_TYPES.map((t) => t.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&")).join("|")})(?=$|[\\s:;])`);
543
+ /**
544
+ * The header keywords MERMAID_VERSION also draws in another casing, each with
545
+ * the part its detector matches as written. Past that part, the detector
546
+ * ignores case (src/diagrams/ishikawa/ishikawaDetector.ts) or matches no
547
+ * further (src/diagrams/requirement/requirementDetector.ts stops at
548
+ * `requirement`), and the grammar's lexer ignores case. Every other keyword
549
+ * draws as listed only: its detector or its grammar reads it as written
550
+ * (wardley-beta's and the railroad types' detectors ignore case, but their
551
+ * grammars refuse `Wardley-beta`).
552
+ */
553
+ const ANY_CASE_AFTER = /* @__PURE__ */ new Map([
554
+ ["ishikawa", ""],
555
+ ["ishikawa-beta", ""],
556
+ ["requirementDiagram", "requirement"],
557
+ ["stateDiagram-v2", "stateDiagram"],
558
+ ["xychart-beta", "xychart"],
559
+ ["sankey-beta", "sankey"]
560
+ ]);
561
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&");
562
+ /** `text` in any casing, a character class per letter: the `i` flag would
563
+ * cover every keyword, and not every browser reads an inline (?i:…) group */
564
+ const anyCase = (text) => [...text].map((character) => character.toLowerCase() === character.toUpperCase() ? escapeRegExp(character) : `[${character.toLowerCase()}${character.toUpperCase()}]`).join("");
565
+ /**
566
+ * The header keyword `type` (a MERMAID_DIAGRAM_TYPES entry) in every casing
567
+ * MERMAID_VERSION draws it in, as the source of a regular expression without
568
+ * flags or anchors. mermaidHeader reads a header with it, and so does the web
569
+ * app's source editor (apps/web/src/lib/mermaid-monarch.ts): the two never
570
+ * disagree about a header.
571
+ */
572
+ function mermaidKeywordPattern(type) {
573
+ const asWritten = ANY_CASE_AFTER.get(type) ?? type;
574
+ return escapeRegExp(asWritten) + anyCase(type.slice(asWritten.length));
575
+ }
576
+ /** a header keyword in a casing mermaid draws, followed by the end of the
577
+ * line, a space, ":" or ";" */
578
+ const HEADER = new RegExp(`^(?:${MERMAID_DIAGRAM_TYPES.map(mermaidKeywordPattern).join("|")})(?=$|[\\s:;])`);
579
+ /** the listed keyword of a header keyword HEADER read in another casing */
580
+ const LISTED_BY_LOWER_CASE = new Map(MERMAID_DIAGRAM_TYPES.map((type) => [type.toLowerCase(), type]));
501
581
  /** mermaid's cleanupText: line breaks to \n, tag attributes single-quoted;
502
582
  * the line structure stays, so the source lines can be matched against it */
503
583
  function cleanup(source) {
@@ -580,10 +660,11 @@ function mermaidHeader(source) {
580
660
  const index = headerIndex(lines);
581
661
  if (index < 0) return void 0;
582
662
  const text = lines[index].trim();
663
+ const keyword = HEADER.exec(text)?.[0];
583
664
  return {
584
665
  line: mermaidSourceLine(source, index + 1),
585
666
  text,
586
- diagramType: HEADER.exec(text)?.[0]
667
+ diagramType: keyword === void 0 ? void 0 : LISTED_BY_LOWER_CASE.get(keyword.toLowerCase())
587
668
  };
588
669
  }
589
670
  //#endregion
@@ -1053,7 +1134,7 @@ async function runCli() {
1053
1134
  /** optional single-model filter (a model file stem) */
1054
1135
  const only = argv[0];
1055
1136
  const rel = (p) => p.startsWith("/") ? relative(ROOT, p) : p;
1056
- const { CONTENT_CONFIG_FILE, contentConfigConflict, discoverModels, legacyContentConfigFile, loadContentConfig } = await import("./content-CAFoHROH.js");
1137
+ const { CONTENT_CONFIG_FILE, contentConfigConflict, discoverModels, legacyContentConfigFile, loadContentConfig } = await import("./content-aBn6QjJK.js");
1057
1138
  const cfg = loadContentConfig(ROOT);
1058
1139
  if (!cfg) {
1059
1140
  console.error(notContentRepoError(ROOT));
package/dist/validate.js CHANGED
@@ -1 +1 @@
1
- export { t as checkBpmnXml, n as checkDmnXml, r as checkModel, i as checkModelBaseline, a as checkSchemaModel, o as runCli } from "./validate-BWa99fy6.js";
1
+ export { t as checkBpmnXml, n as checkDmnXml, r as checkModel, i as checkModelBaseline, a as checkSchemaModel, o as runCli } from "./validate-HHrcxggb.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miragon/design-iq-validator",
3
- "version": "5.10.0",
3
+ "version": "5.11.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, the schema check of Team Topologies, Context Maps and Value Chains, and a parse check for every other notation. Runs against any checkout via --root; never executes content-repo code.",
6
6
  "license": "MIT",