@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 +16 -13
- package/dist/cli.js +1 -1
- package/dist/{content-CE4SulnP.js → content-B0u0kwZN.js} +1 -1
- package/dist/{content-Bu1bNrez.js → content-DfRb_tAL.js} +3 -1
- package/dist/{validate-BqkEd0ma.js → validate-CUTctX4d.js} +82 -92
- package/dist/validate.js +1 -1
- package/package.json +5 -5
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
|
|
29
|
-
|
|
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,
|
|
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.
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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 +1 @@
|
|
|
1
|
-
export { t as CONTENT_CONFIG_FILE, i as contentConfigConflict, a as discoverModels, o as legacyContentConfigFile, s as loadContentConfig } from "./content-
|
|
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
|
|
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-
|
|
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
|
|
496
|
-
* whitespace
|
|
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
|
|
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
|
|
510
|
-
const sourceLines =
|
|
511
|
-
|
|
512
|
-
|
|
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
|
|
564
|
+
const lineText = codeLines[i].trim();
|
|
516
565
|
let k = next;
|
|
517
|
-
while (k < sourceLines.length && sourceLines[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
|
|
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
|
-
|
|
920
|
-
return
|
|
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
|
-
|
|
934
|
+
if (!parsed.ok) return (parsed.issues ?? [{
|
|
930
935
|
path: [],
|
|
931
936
|
message: parsed.error
|
|
932
|
-
}];
|
|
933
|
-
|
|
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 [...
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
|
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.
|
|
38
|
-
"@miragon/team-topologies-schema-model": "0.
|
|
39
|
-
"@miragon/value-chain-schema-model": "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"
|