@miragon/design-iq-validator 5.5.0 → 5.6.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
@@ -3,13 +3,47 @@
3
3
  Deterministic validator for designIQ content repositories: `designiq.yml` discovery, BPMN
4
4
  structural checks (flow soundness, complete BPMNDI coverage), DMN structure and DMNDI coverage,
5
5
  the generic cross-model reference rule (every required reference — `callActivity` calls,
6
- `businessRuleTask` decides — must resolve in the repo), and a per-mediaKind parse baseline for
7
- every other registered notation (a broken `.tt`/`.vc.json` is an ERROR; DSL notations are
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`,
11
- `refs/dangling`). It treats the target repo as pure data — it never executes content-repo code.
12
- Exit code 0 = no errors (warnings allowed), 1 = errors.
6
+ `businessRuleTask` decides — must resolve in the repo), the schema check of the Miragon JSON
7
+ notations, and a per-mediaKind parse baseline for every other registered notation (a file that
8
+ is no JSON or XML is an ERROR; DSL notations are lenient by design; a `mermaid` fence in a
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
11
+ carries a stable `ruleId` (e.g. `bpmn/flow`, `dmn/di`, `refs/dangling`). It treats the target
12
+ repo as pure data — it never executes content-repo code. Exit code 0 = no errors (warnings
13
+ allowed), 1 = errors.
14
+
15
+ ## Team Topologies, Context Maps and Value Chains
16
+
17
+ A `.tt`/`.ttm.json`, `.cm.json` or `.vc.json` file is parsed with the schema model its canvas
18
+ opens it with (`@miragon/team-topologies-schema-model`, `@miragon/context-maps-schema-model`,
19
+ `@miragon/value-chain-schema-model`). Whatever the schema model refuses, the canvas cannot open,
20
+ so each refusal is an ERROR (`team-topology/schema`, `context-map/schema`,
21
+ `value-chain/schema`) that names the offending value by its JSON path, e.g.
22
+ `nodes.1.id: duplicate id "a"`. Refused among others:
23
+
24
+ - a duplicate id, and the ids the diagram keeps for itself: `tt-root`, `cm-root`, `vc-root`,
25
+ every id starting with `__implicitroot`, and `assoc_<annotation id>` in a Team Topology;
26
+ - in all three notations, an id that names a built-in JavaScript object property such as
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;
30
+ - in a Context Map: a JSON list instead of an object, a `version` newer than 1, coordinates or
31
+ sizes beyond 1,000,000;
32
+ - 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.
34
+
35
+ A Context Map's strategic-DDD rules (`validateDocument`, e.g. `protect-or-conform`,
36
+ `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)`.
40
+
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.
13
47
 
14
48
  ## Usage
15
49
 
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as runCli } from "./validate-DjtZJTGk.js";
2
+ import { o as runCli } from "./validate-BqkEd0ma.js";
3
3
  //#region src/cli.ts
4
4
  /**
5
5
  * CLI entry for the platform validator. A dedicated entry file (instead of an
@@ -537,20 +537,23 @@ function extractWardley(raw) {
537
537
  }
538
538
  function extractTeamTopology(raw) {
539
539
  const data = JSON.parse(raw);
540
+ const list = (v) => Array.isArray(v) ? v : [];
541
+ const teams = list(data.nodes).map((n) => ({
542
+ id: n.id,
543
+ type: n.type ?? "team",
544
+ name: n.label ?? n.name,
545
+ extra: n.description ? { description: n.description } : void 0
546
+ }));
547
+ const interactions = list(data.interactions).filter((i) => i !== null && typeof i === "object" && "position" in i && "size" in i).map((i) => ({
548
+ id: i.id,
549
+ type: "interaction",
550
+ name: i.label,
551
+ extra: { mode: i.mode }
552
+ }));
540
553
  return {
541
554
  notation: "team-topology",
542
- nodes: (data.nodes ?? []).map((n) => ({
543
- id: n.id,
544
- type: n.type ?? "team",
545
- name: n.label ?? n.name,
546
- extra: n.description ? { description: n.description } : void 0
547
- })),
548
- edges: (data.edges ?? []).map((e, i) => ({
549
- id: e.id ?? `edge-${i}`,
550
- from: e.source ?? e.from,
551
- to: e.target ?? e.to,
552
- kind: e.interaction ?? e.type ?? "interaction"
553
- }))
555
+ nodes: [...teams, ...interactions],
556
+ edges: []
554
557
  };
555
558
  }
556
559
  const STORM_ID_PREFIX = {
@@ -801,7 +804,7 @@ function extractValueChain(raw) {
801
804
  nodes: (data.elements ?? []).map((el) => ({
802
805
  id: el.id,
803
806
  type: el.elementType ?? "element",
804
- name: el.label ?? el.name
807
+ name: el.name
805
808
  })),
806
809
  edges: (data.connections ?? []).map((c, i) => ({
807
810
  id: c.id ?? `conn-${i}`,
@@ -1 +1 @@
1
- export { t as CONTENT_CONFIG_FILE, i as contentConfigConflict, a as discoverModels, o as legacyContentConfigFile, s as loadContentConfig } from "./content-Bsv9ZLFd.js";
1
+ export { t as CONTENT_CONFIG_FILE, i as contentConfigConflict, a as discoverModels, o as legacyContentConfigFile, s as loadContentConfig } from "./content-Bu1bNrez.js";
@@ -1,6 +1,9 @@
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-Bsv9ZLFd.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-Bu1bNrez.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";
6
+ import { documentIssues } from "@miragon/value-chain-schema-model";
4
7
  //#region ../notations/refs.ts
5
8
  const REF_EMITTERS = { bpmn: (graph) => {
6
9
  const refs = [];
@@ -718,7 +721,7 @@ function checkBpmnXml(raw, opts = {}) {
718
721
  *
719
722
  * Deliberately NOT here: FEEL semantics, hit-policy reachability, dead wiring.
720
723
  * Those need the FEEL engine and live in @designiq/decisions (`analyze_decision`),
721
- * which would drag a browser-side dependency into this zero-dep CLI.
724
+ * which would drag a browser-side dependency into this CLI.
722
725
  */
723
726
  function checkDmnXml(raw, opts = {}) {
724
727
  const file = opts.file ?? "<dmn>";
@@ -809,8 +812,8 @@ function mermaidFenceProblem(content, line) {
809
812
  * notation that is not .bpmn/.dmn): a discovered model must at minimum parse
810
813
  * per its notation's mediaKind — a broken .tt/.vc.json used to pass
811
814
  * validation untouched. Pure: no filesystem, no process.exit. Deliberately
812
- * mechanical: notation-specific structure checks arrive with the per-notation
813
- * checkers (epic #118, step 3).
815
+ * mechanical: notation-specific structure checks are the per-notation
816
+ * checkers' (epic #118, step 3; checkSchemaModel runs this gate first).
814
817
  */
815
818
  function checkModelBaseline(raw, opts) {
816
819
  const file = opts.file ?? `<${opts.notation}>`;
@@ -856,18 +859,162 @@ function checkModelBaseline(raw, opts) {
856
859
  }
857
860
  return findings;
858
861
  }
862
+ /** a schema model's issue location as the canvases print it ("nodes.1.id"),
863
+ * "(root)" for the document itself */
864
+ const issuePath = (path) => path.join(".") || "(root)";
865
+ const schemaError = (ruleId, file) => (issue) => ({
866
+ severity: "ERROR",
867
+ ruleId,
868
+ file,
869
+ message: `${issuePath(issue.path)}: ${issue.message}`
870
+ });
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
+ function checkTeamTopology(raw, file) {
917
+ const parsed = parseDocument(JSON.parse(raw));
918
+ 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));
926
+ }
927
+ function checkContextMap(raw, file) {
928
+ const parsed = parseDocumentText(raw);
929
+ const issues = parsed.ok ? [] : parsed.issues ?? [{
930
+ path: [],
931
+ 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
+ };
946
+ 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
+ })];
956
+ }
957
+ function checkValueChain(raw, file) {
958
+ return documentIssues(JSON.parse(raw)).map(schemaError("value-chain/schema", file));
959
+ }
960
+ /** each JSON notation's schema model, read the way its canvas reads the file
961
+ * (a Map, not an object: the notation id may be a caller's free text such
962
+ * as "constructor") */
963
+ const SCHEMA_CHECKS = /* @__PURE__ */ new Map([
964
+ ["team-topology", {
965
+ ruleId: "team-topology/schema",
966
+ check: checkTeamTopology
967
+ }],
968
+ ["context-map", {
969
+ ruleId: "context-map/schema",
970
+ check: checkContextMap
971
+ }],
972
+ ["value-chain", {
973
+ ruleId: "value-chain/schema",
974
+ check: checkValueChain
975
+ }]
976
+ ]);
977
+ /**
978
+ * The schema check of a Miragon JSON notation (Team Topology, Context Map,
979
+ * Value Chain): the file is parsed with the notation's schema model, the
980
+ * same parse its canvas opens it with, so everything the model refuses (a
981
+ * duplicate or reserved id, a coordinate out of range, a newer version) is
982
+ * an ERROR — a save would otherwise land a file the canvas cannot open.
983
+ * Text that is no JSON keeps the baseline's finding, and a notation without
984
+ * a schema model gets the baseline alone. Pure: no filesystem, no
985
+ * process.exit.
986
+ */
987
+ function checkSchemaModel(raw, opts) {
988
+ const baseline = checkModelBaseline(raw, opts);
989
+ const schema = SCHEMA_CHECKS.get(opts.notation);
990
+ if (!schema || baseline.length > 0 || byId(opts.notation)?.docShape === "structured") return baseline;
991
+ const file = opts.file ?? `<${opts.notation}>`;
992
+ try {
993
+ return schema.check(raw, file);
994
+ } catch (error) {
995
+ const reason = error instanceof Error ? error.message : String(error);
996
+ return [{
997
+ severity: "ERROR",
998
+ ruleId: schema.ruleId,
999
+ file,
1000
+ message: `(root): could not be checked: ${reason}`
1001
+ }];
1002
+ }
1003
+ }
859
1004
  /**
860
1005
  * THE one check dispatch: the platform check for whatever notation `ctx.path`
861
- * is — full checkers for BPMN/DMN, the mediaKind baseline for everything
862
- * else, plus the generic dangling-reference rule for ANY notation with a
863
- * refs emitter; `undefined` when the path matches no registered notation
864
- * (the file is not a model; nothing to check). Consumed by the CLI (runCli)
865
- * AND the Live Host's save gate (application/content.ts) so both always agree.
1006
+ * is — full checkers for BPMN/DMN, the schema model for the Miragon JSON
1007
+ * notations, the mediaKind baseline for everything else, plus the generic
1008
+ * dangling-reference rule for ANY notation with a refs emitter; `undefined`
1009
+ * when the path matches no registered notation (the file is not a model;
1010
+ * nothing to check) or `ctx.notation` names none (a mistyped id must not
1011
+ * pass a file unchecked). Consumed by the CLI (runCli) AND the Live Host's
1012
+ * save gate (application/content.ts) so both always agree.
866
1013
  */
867
1014
  function checkModel(raw, ctx) {
868
1015
  const notation = ctx.notation ?? byExtension(ctx.path)?.id;
869
- if (!notation) return void 0;
870
- return [...notation === "bpmn" ? checkBpmnXml(raw, { file: ctx.path }).findings : notation === "dmn" ? checkDmnXml(raw, { file: ctx.path }).findings : checkModelBaseline(raw, {
1016
+ if (!notation || !byId(notation)) return void 0;
1017
+ return [...notation === "bpmn" ? checkBpmnXml(raw, { file: ctx.path }).findings : notation === "dmn" ? checkDmnXml(raw, { file: ctx.path }).findings : checkSchemaModel(raw, {
871
1018
  file: ctx.path,
872
1019
  notation
873
1020
  }), ...checkRefs(notation, raw, ctx)];
@@ -916,7 +1063,7 @@ async function runCli() {
916
1063
  /** optional single-model filter (a model file stem) */
917
1064
  const only = argv[0];
918
1065
  const rel = (p) => p.startsWith("/") ? relative(ROOT, p) : p;
919
- const { CONTENT_CONFIG_FILE, contentConfigConflict, discoverModels, legacyContentConfigFile, loadContentConfig } = await import("./content-wjC_epKd.js");
1066
+ const { CONTENT_CONFIG_FILE, contentConfigConflict, discoverModels, legacyContentConfigFile, loadContentConfig } = await import("./content-CE4SulnP.js");
920
1067
  const cfg = loadContentConfig(ROOT);
921
1068
  if (!cfg) {
922
1069
  console.error(notContentRepoError(ROOT));
@@ -979,4 +1126,4 @@ async function runCli() {
979
1126
  process.exit(errorCount === 0 ? 0 : 1);
980
1127
  }
981
1128
  //#endregion
982
- export { runCli as a, checkModelBaseline as i, checkDmnXml as n, checkModel as r, checkBpmnXml as t };
1129
+ export { checkSchemaModel as a, checkModelBaseline as i, checkDmnXml as n, runCli as o, checkModel as r, checkBpmnXml as t };
package/dist/validate.js CHANGED
@@ -1 +1 @@
1
- export { t as checkBpmnXml, n as checkDmnXml, r as checkModel, i as checkModelBaseline, a as runCli } from "./validate-DjtZJTGk.js";
1
+ export { t as checkBpmnXml, n as checkDmnXml, r as checkModel, i as checkModelBaseline, a as checkSchemaModel, o as runCli } from "./validate-BqkEd0ma.js";
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@miragon/design-iq-validator",
3
- "version": "5.5.0",
3
+ "version": "5.6.0",
4
4
  "type": "module",
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.",
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",
7
7
  "repository": {
8
8
  "type": "git",
@@ -32,10 +32,14 @@
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.",
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.",
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
40
  "fast-xml-parser": "5.11.2",
38
- "yaml": "2.9.1"
41
+ "yaml": "2.9.1",
42
+ "zod": "4.6.7"
39
43
  },
40
44
  "devDependencies": {
41
45
  "tsdown": "0.23.1",