@miragon/design-iq-validator 5.6.0 → 5.8.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
@@ -25,25 +25,28 @@ so each refusal is an ERROR (`team-topology/schema`, `context-map/schema`,
25
25
  every id starting with `__implicitroot`, and `assoc_<annotation id>` in a Team Topology;
26
26
  - in all three notations, an id that names a built-in JavaScript object property such as
27
27
  `constructor`, `toString` or `__proto__`: the canvas keeps its elements in a plain object and
28
- cannot import it. The Team Topology and Context Map schema models still take these ids, so the
29
- validator refuses them itself, under the same rule;
28
+ cannot import it;
29
+ - in a Team Topology whose `version` is not 1, a file without `version` included: an interaction
30
+ without `position` or `size`, at the missing field (`interactions.0.size`). Opened anyway, it
31
+ would drop out of the file with the next save. A version 1 file still loses its edge
32
+ interactions, which have no geometry;
30
33
  - in a Context Map: a JSON list instead of an object, a `version` newer than 1, coordinates or
31
- sizes beyond 1,000,000;
34
+ sizes beyond 1,000,000, and a repeated id at the path of the repeat, e.g.
35
+ `contexts.2.id: duplicate id "a"; ids must be unique across contexts and relationships`;
32
36
  - in a Value Chain: keys other than `x`, `y`, `width`, `height` in `bounds` and other than `x`,
33
- `y` in a waypoint, coordinates beyond ±1e9, and a `__proto__` key anywhere.
37
+ `y` in a waypoint, coordinates beyond ±1e9, a `__proto__` key anywhere, and nesting deeper than
38
+ 100 levels;
39
+ - in a Team Topology and a Context Map: a document nested too deep for the schema model's
40
+ serializer, which every canvas save runs, as `(root): could not be checked: …`.
34
41
 
35
42
  A Context Map's strategic-DDD rules (`validateDocument`, e.g. `protect-or-conform`,
36
43
  `endpoints-on-map`) are WARNs, `context-map/semantics`, with the rule named at the end of the
37
- message: the canvas opens such a map and marks the finding. A repeated id is the exception: the
38
- canvas refuses it, so it is an ERROR (`context-map/schema`) at the path of the repeat, e.g.
39
- `contexts.2.id: Duplicate id "a". (unique-ids)`.
44
+ message: the canvas opens such a map and marks the finding.
40
45
 
41
- Paths count in the document as the schema model reads it. A Team Topology drops a legacy
42
- interaction without `position` or `size` before the check, so the index of a later interaction
43
- counts without it. Some rules only run once the document's shape is valid: the id rules of a Team
44
- Topology and a Value Chain, and in a Context Map the repeated ids and the strategic-DDD rules.
45
- Fixing the reported errors can therefore bring up further ones, so check again until the file is
46
- clean.
46
+ Paths count in the file, a Team Topology's interactions included. Some rules only run once the
47
+ document's shape is valid: the id rules of a Team Topology and a Value Chain, and a Context Map's
48
+ strategic-DDD rules. Fixing the reported errors can therefore bring up further ones, so check
49
+ again until the file is clean.
47
50
 
48
51
  ## Usage
49
52
 
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { o as runCli } from "./validate-BqkEd0ma.js";
2
+ import { o as runCli } from "./validate-CUTctX4d.js";
3
3
  //#region src/cli.ts
4
4
  /**
5
5
  * CLI entry for the platform validator. A dedicated entry file (instead of an
@@ -1 +1 @@
1
- export { t as CONTENT_CONFIG_FILE, i as contentConfigConflict, a as discoverModels, o as legacyContentConfigFile, s as loadContentConfig } from "./content-Bu1bNrez.js";
1
+ export { t as CONTENT_CONFIG_FILE, i as contentConfigConflict, a as discoverModels, o as legacyContentConfigFile, s as loadContentConfig } from "./content-DfRb_tAL.js";
@@ -535,6 +535,7 @@ function extractWardley(raw) {
535
535
  edges
536
536
  };
537
537
  }
538
+ const TT_EDGE_INTERACTIONS_VERSION = 1;
538
539
  function extractTeamTopology(raw) {
539
540
  const data = JSON.parse(raw);
540
541
  const list = (v) => Array.isArray(v) ? v : [];
@@ -544,7 +545,8 @@ function extractTeamTopology(raw) {
544
545
  name: n.label ?? n.name,
545
546
  extra: n.description ? { description: n.description } : void 0
546
547
  }));
547
- const interactions = list(data.interactions).filter((i) => i !== null && typeof i === "object" && "position" in i && "size" in i).map((i) => ({
548
+ const placed = (i) => "position" in i && "size" in i;
549
+ const interactions = list(data.interactions).filter((i) => i !== null && typeof i === "object").filter((i) => data.version !== TT_EDGE_INTERACTIONS_VERSION || placed(i)).map((i) => ({
548
550
  id: i.id,
549
551
  type: "interaction",
550
552
  name: i.label,
@@ -1,8 +1,8 @@
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-Bu1bNrez.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-DfRb_tAL.js";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { relative, resolve } from "node:path";
4
- import { parseDocumentText, validateDocument } from "@miragon/context-maps-schema-model";
5
- import { parseDocument } from "@miragon/team-topologies-schema-model";
4
+ import { parseDocumentText, serializeDocumentText, validateDocument } from "@miragon/context-maps-schema-model";
5
+ import { parseDocument, serializeDocument } from "@miragon/team-topologies-schema-model";
6
6
  import { documentIssues } from "@miragon/value-chain-schema-model";
7
7
  //#region ../notations/refs.ts
8
8
  const REF_EMITTERS = { bpmn: (graph) => {
@@ -479,11 +479,23 @@ const MERMAID_DIAGRAM_TYPES = [
479
479
  "railroad-beta",
480
480
  "railroad-ebnf-beta",
481
481
  "railroad-abnf-beta",
482
- "railroad-peg-beta"
482
+ "railroad-peg-beta",
483
+ "usecase-beta",
484
+ "agentflow-beta"
483
485
  ];
484
486
  const FRONT_MATTER = /^([^\S\n\r]*)-{3}\s*[\n\r](.*?)[\n\r]\1-{3}\s*[\n\r]+/s;
485
487
  const DIRECTIVE = /%{2}{\s*(?:(\w+)\s*:|(\w+))\s*(?:(\w+)|((?:(?!}%{2}).|\r?\n)*))?\s*(?:}%{2})?/gi;
486
488
  const COMMENT_LINE = /^\s*%%(?!{)[^\n]+\n?/gm;
489
+ /**
490
+ * The diagram types whose parser reads the comment lines too: their db sets
491
+ * preserveCommentsWhenParsing (src/Diagram.ts), so their errors count those
492
+ * lines. mermaid's detectors for them (src/diagrams/eventmodeling/detector.ts,
493
+ * src/diagrams/agentflow/afDetector.ts), against the header line.
494
+ */
495
+ const KEEPS_COMMENTS = /^(?:eventmodeling|agentflow-beta\b)/;
496
+ /** mermaid's default maxTextSize, which apps/web keeps (a secure key): past
497
+ * it, mermaid parses even those types without their comment lines */
498
+ const MAX_TEXT_SIZE = 5e4;
487
499
  /** a header keyword, followed by the end of the line, a space, ":" or ";" */
488
500
  const HEADER = new RegExp(`^(?:${MERMAID_DIAGRAM_TYPES.map((t) => t.replace(/[.*+?^${}()|[\]\\-]/g, "\\$&")).join("|")})(?=$|[\\s:;])`);
489
501
  /** mermaid's cleanupText: line breaks to \n, tag attributes single-quoted;
@@ -491,11 +503,34 @@ const HEADER = new RegExp(`^(?:${MERMAID_DIAGRAM_TYPES.map((t) => t.replace(/[.*
491
503
  function cleanup(source) {
492
504
  return source.replace(/\r\n?/g, "\n").replace(/<(\w+)([^>]*)>/g, (_tag, name, attrs) => `<${name}${attrs.replace(/="([^"]*)"/g, "='$1'")}>`);
493
505
  }
506
+ /** the first line that is neither blank nor a bare `%%` comment (mermaid's
507
+ * type detection skips those too), -1 when there is none */
508
+ function headerIndex(lines) {
509
+ return lines.findIndex((l) => l.trim() !== "" && !l.trim().startsWith("%%"));
510
+ }
511
+ /** mermaid's preprocess (src/preprocess.ts), step by step */
512
+ function preprocess(source) {
513
+ const text = cleanup(source);
514
+ const frontMatter = FRONT_MATTER.exec(text)?.[0] ?? "";
515
+ const body = text.slice(frontMatter.length);
516
+ const withComments = body.replace(DIRECTIVE, "");
517
+ const cleaned = withComments.replace(COMMENT_LINE, "").trimStart();
518
+ const cleanedLines = cleaned.split("\n");
519
+ const header = cleanedLines[headerIndex(cleanedLines)]?.trim() ?? "";
520
+ const keepsComments = KEEPS_COMMENTS.test(header) && withComments.length <= MAX_TEXT_SIZE;
521
+ return {
522
+ text,
523
+ frontMatterLines: frontMatter.split("\n").length - 1,
524
+ body,
525
+ keepsComments,
526
+ code: keepsComments ? withComments : cleaned
527
+ };
528
+ }
494
529
  /** The text mermaid's diagram parser reads: the source after mermaid's
495
- * preprocess (front matter, directives and comment lines removed, leading
496
- * whitespace trimmed). */
530
+ * preprocess, its front matter and directives removed and, for all but the
531
+ * KEEPS_COMMENTS types, its comment lines and leading whitespace too. */
497
532
  function mermaidCode(source) {
498
- return cleanup(source).replace(FRONT_MATTER, "").replace(DIRECTIVE, "").replace(COMMENT_LINE, "").trimStart();
533
+ return preprocess(source).code;
499
534
  }
500
535
  /**
501
536
  * The 1-based source line of the 1-based line `codeLine` of mermaidCode(source).
@@ -503,18 +538,32 @@ function mermaidCode(source) {
503
538
  * that spans lines), so the code is the source's lines in order: each code
504
539
  * line is matched to the next source line with the same text. A line the
505
540
  * preprocess rewrote matches nothing and takes the next line; a line past the
506
- * end clamps to the last one.
541
+ * end clamps to the last one. Code that kept its comment lines lost no line
542
+ * but the front matter's and a directive's inner line breaks, so its lines
543
+ * are counted instead: matching by text would take a line a directive
544
+ * emptied for a later blank line.
507
545
  */
508
546
  function mermaidSourceLine(source, codeLine) {
509
- const cleaned = cleanup(source);
510
- const sourceLines = cleaned.split("\n").map((l) => l.trim());
511
- const codeLines = mermaidCode(source).split("\n");
512
- let next = (cleaned.match(FRONT_MATTER)?.[0] ?? "").split("\n").length - 1;
547
+ const { text, frontMatterLines, body, keepsComments, code } = preprocess(source);
548
+ const sourceLines = text.split("\n").map((l) => l.trim());
549
+ if (keepsComments) {
550
+ const joined = /* @__PURE__ */ new Set();
551
+ for (const directive of body.matchAll(DIRECTIVE)) for (let i = directive.index; i < directive.index + directive[0].length; i++) if (body[i] === "\n") joined.add(i);
552
+ let line = frontMatterLines;
553
+ for (let i = 0, breaks = 0; i < body.length && breaks < codeLine - 1; i++) {
554
+ if (body[i] !== "\n") continue;
555
+ line++;
556
+ if (!joined.has(i)) breaks++;
557
+ }
558
+ return Math.min(line, sourceLines.length - 1) + 1;
559
+ }
560
+ const codeLines = code.split("\n");
561
+ let next = frontMatterLines;
513
562
  let at = next;
514
563
  for (let i = 0; i < Math.min(codeLine, codeLines.length); i++) {
515
- const text = codeLines[i].trim();
564
+ const lineText = codeLines[i].trim();
516
565
  let k = next;
517
- while (k < sourceLines.length && sourceLines[k] !== text) k++;
566
+ while (k < sourceLines.length && sourceLines[k] !== lineText) k++;
518
567
  at = k < sourceLines.length ? k : next;
519
568
  next = at + 1;
520
569
  }
@@ -528,7 +577,7 @@ function mermaidSourceLine(source, codeLine) {
528
577
  */
529
578
  function mermaidHeader(source) {
530
579
  const lines = mermaidCode(source).split("\n");
531
- const index = lines.findIndex((l) => l.trim() !== "" && !l.trim().startsWith("%%"));
580
+ const index = headerIndex(lines);
532
581
  if (index < 0) return void 0;
533
582
  const text = lines[index].trim();
534
583
  return {
@@ -868,91 +917,32 @@ const schemaError = (ruleId, file) => (issue) => ({
868
917
  file,
869
918
  message: `${issuePath(issue.path)}: ${issue.message}`
870
919
  });
871
- /**
872
- * The ids a Team Topology or Context Map canvas cannot hold although its
873
- * schema model takes them: diagram-js keeps its elements in a plain object,
874
- * so an id that names a built-in object property ("constructor",
875
- * "__proto__") reads as already added and the import fails. The Value Chain
876
- * schema model refuses these ids itself; the other two do not yet.
877
- */
878
- function builtInPropertyIds(collections) {
879
- return Object.entries(collections).flatMap(([collection, elements]) => (Array.isArray(elements) ? elements : []).flatMap((element, index) => {
880
- const id = element?.id;
881
- return typeof id === "string" && Object.hasOwn(Object.prototype, id) ? [{
882
- path: [
883
- collection,
884
- index,
885
- "id"
886
- ],
887
- message: `the id "${id}" is reserved: it names a built-in property of JavaScript objects`
888
- }] : [];
889
- }));
890
- }
891
- /**
892
- * The context-map rules the canvas enforces itself: its importer refuses a
893
- * repeated id (CmImporter.assertImportable), so the file does not open. The
894
- * other strategic-DDD rules are advisory there: the canvas opens the map and
895
- * marks the finding, so a save must not refuse it either.
896
- */
897
- const CONTEXT_MAP_REFUSED_RULES = /* @__PURE__ */ new Set(["unique-ids"]);
898
- /** the path of every repeated id, in the order validateDocument's unique-ids
899
- * rule reports them (contexts, then relationships): its findings name no
900
- * position, and an agent must know which of two entries to change */
901
- function repeatedIdPaths({ contexts, relationships }) {
902
- const seen = /* @__PURE__ */ new Set();
903
- return Object.entries({
904
- contexts,
905
- relationships
906
- }).flatMap(([collection, elements]) => elements.flatMap(({ id }, index) => {
907
- const repeated = seen.has(id);
908
- seen.add(id);
909
- return repeated ? [issuePath([
910
- collection,
911
- index,
912
- "id"
913
- ])] : [];
914
- }));
915
- }
916
920
  function checkTeamTopology(raw, file) {
917
921
  const parsed = parseDocument(JSON.parse(raw));
918
922
  if (!parsed.ok) return parsed.issues.map(schemaError("team-topology/schema", file));
919
- const { nodes, interactions, flows, annotations } = parsed.document;
920
- return builtInPropertyIds({
921
- nodes,
922
- interactions,
923
- flows,
924
- annotations
925
- }).map(schemaError("team-topology/schema", file));
923
+ serializeDocument(parsed.document);
924
+ return [];
926
925
  }
926
+ /**
927
+ * The schema model refuses what the canvas cannot draw (a repeated or
928
+ * reserved id among them), the rest of its strategic-DDD rules are
929
+ * advisory: the canvas opens the map and marks the finding, so a save must
930
+ * not refuse it either.
931
+ */
927
932
  function checkContextMap(raw, file) {
928
933
  const parsed = parseDocumentText(raw);
929
- const issues = parsed.ok ? [] : parsed.issues ?? [{
934
+ if (!parsed.ok) return (parsed.issues ?? [{
930
935
  path: [],
931
936
  message: parsed.error
932
- }];
933
- if (issues.some(({ path }) => path.length === 0 || path[0] === "version")) return issues.map(schemaError("context-map/schema", file));
934
- const { contexts, relationships } = JSON.parse(raw);
935
- const schemaFindings = [...issues, ...builtInPropertyIds({
936
- contexts,
937
- relationships
938
- })].map(schemaError("context-map/schema", file));
939
- if (!parsed.ok) return schemaFindings;
940
- const repeatedIds = repeatedIdPaths(parsed.document);
941
- const subject = (finding) => {
942
- const path = finding.rule === "unique-ids" ? repeatedIds.shift() : void 0;
943
- if (path) return `${path}: `;
944
- return finding.relationshipId === void 0 ? "" : `relationship "${finding.relationshipId}": `;
945
- };
937
+ }]).map(schemaError("context-map/schema", file));
938
+ serializeDocumentText(parsed.document);
946
939
  const { errors, warnings } = validateDocument(parsed.document);
947
- return [...schemaFindings, ...[...errors, ...warnings].map((finding) => {
948
- const refused = CONTEXT_MAP_REFUSED_RULES.has(finding.rule);
949
- return {
950
- severity: refused ? "ERROR" : "WARN",
951
- ruleId: refused ? "context-map/schema" : "context-map/semantics",
952
- file,
953
- message: `${subject(finding)}${finding.message} (${finding.rule})`
954
- };
955
- })];
940
+ return [...errors, ...warnings].map((finding) => ({
941
+ severity: "WARN",
942
+ ruleId: "context-map/semantics",
943
+ file,
944
+ message: `${finding.relationshipId === void 0 ? "" : `relationship "${finding.relationshipId}": `}${finding.message} (${finding.rule})`
945
+ }));
956
946
  }
957
947
  function checkValueChain(raw, file) {
958
948
  return documentIssues(JSON.parse(raw)).map(schemaError("value-chain/schema", file));
@@ -1063,7 +1053,7 @@ async function runCli() {
1063
1053
  /** optional single-model filter (a model file stem) */
1064
1054
  const only = argv[0];
1065
1055
  const rel = (p) => p.startsWith("/") ? relative(ROOT, p) : p;
1066
- const { CONTENT_CONFIG_FILE, contentConfigConflict, discoverModels, legacyContentConfigFile, loadContentConfig } = await import("./content-CE4SulnP.js");
1056
+ const { CONTENT_CONFIG_FILE, contentConfigConflict, discoverModels, legacyContentConfigFile, loadContentConfig } = await import("./content-B0u0kwZN.js");
1067
1057
  const cfg = loadContentConfig(ROOT);
1068
1058
  if (!cfg) {
1069
1059
  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-BqkEd0ma.js";
1
+ export { t as checkBpmnXml, n as checkDmnXml, r as checkModel, i as checkModelBaseline, a as checkSchemaModel, o as runCli } from "./validate-CUTctX4d.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miragon/design-iq-validator",
3
- "version": "5.6.0",
3
+ "version": "5.8.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",
@@ -32,11 +32,11 @@
32
32
  "publishConfig": {
33
33
  "access": "public"
34
34
  },
35
- "//": "fast-xml-parser stays a RUNTIME dependency although src/ no longer imports it directly: tsdown inlines @designiq/notations (whose extract.ts imports it) but keeps npm deps external, so the published dist/ resolves it from here. Removing it breaks the npm artifact at import time while the workspace keeps working. zod is a RUNTIME dependency for the same reason: src/ never imports it, but the team-topologies and context-maps schema models take it as a peer, and the published package is what provides it.",
35
+ "//": "fast-xml-parser stays a RUNTIME dependency although src/ no longer imports it directly: tsdown inlines @designiq/notations (whose extract.ts imports it) but keeps npm deps external, so the published dist/ resolves it from here. Removing it breaks the npm artifact at import time while the workspace keeps working. zod is a RUNTIME dependency for the same reason: src/ never imports it, but the team-topologies, context-maps and value-chain schema models take it as a peer, and the published package is what provides it.",
36
36
  "dependencies": {
37
- "@miragon/context-maps-schema-model": "0.3.0",
38
- "@miragon/team-topologies-schema-model": "0.11.2",
39
- "@miragon/value-chain-schema-model": "0.5.0",
37
+ "@miragon/context-maps-schema-model": "0.4.0",
38
+ "@miragon/team-topologies-schema-model": "0.12.0",
39
+ "@miragon/value-chain-schema-model": "0.6.0",
40
40
  "fast-xml-parser": "5.11.2",
41
41
  "yaml": "2.9.1",
42
42
  "zod": "4.6.7"