From b0df3e0c8faf0728e558cae26451754761adcc51 Mon Sep 17 00:00:00 2001 From: anhnh2002 Date: Wed, 28 Oct 2026 09:25:37 +0700 Subject: [PATCH 1/2] fix: repair Mermaid labels and report validation accurately --- .github/workflows/ci.yml | 14 + .gitignore | 7 + codewiki/mcp/tools/doc_writer.py | 22 +- .../src/be/agent_tools/str_replace_editor.py | 71 +- codewiki/src/be/caw_toolkit.py | 19 +- codewiki/src/be/documentation_generator.py | 66 +- codewiki/src/be/mermaid_support.py | 188 ++ codewiki/src/be/prompt_template.py | 9 + codewiki/src/be/updater/prompts.py | 5 + codewiki/src/be/utils.py | 274 +-- skills/codewiki-wiki-generator/SKILL.md | 26 +- tests/mermaid/check.mjs | 24 + tests/mermaid/fixtures.json | 158 ++ tests/mermaid/package-lock.json | 2011 +++++++++++++++++ tests/mermaid/package.json | 12 + tests/test_mermaid_validation.py | 293 +++ tests/test_mermaid_writers.py | 275 +++ 17 files changed, 3278 insertions(+), 196 deletions(-) create mode 100644 codewiki/src/be/mermaid_support.py create mode 100644 tests/mermaid/check.mjs create mode 100644 tests/mermaid/fixtures.json create mode 100644 tests/mermaid/package-lock.json create mode 100644 tests/mermaid/package.json create mode 100644 tests/test_mermaid_validation.py create mode 100644 tests/test_mermaid_writers.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ee8d8b1a..f48484df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,20 @@ permissions: contents: read jobs: + mermaid: + name: Mermaid syntax + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: npm + cache-dependency-path: tests/mermaid/package-lock.json + - run: npm ci --prefix tests/mermaid --ignore-scripts --no-audit --no-fund + - run: npm test --prefix tests/mermaid + test: name: Test runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 9402bacf..09d9a25e 100644 --- a/.gitignore +++ b/.gitignore @@ -91,3 +91,10 @@ Thumbs.db !tests/test_updater_write_guard.py !tests/test_updater_orchestrator.py !tests/test_usage_ledger.py + +# Mermaid regression tests and development-only parser harness +!tests/test_mermaid_validation.py +!tests/test_mermaid_writers.py +!tests/mermaid/ +!tests/mermaid/** +tests/mermaid/node_modules/ diff --git a/codewiki/mcp/tools/doc_writer.py b/codewiki/mcp/tools/doc_writer.py index da6889e6..d7450904 100644 --- a/codewiki/mcp/tools/doc_writer.py +++ b/codewiki/mcp/tools/doc_writer.py @@ -45,14 +45,18 @@ def _ensure_parent_dirs(path: Path) -> None: path.parent.mkdir(parents=True, exist_ok=True) -async def _validate_mermaid(file_path: str, relative_path: str) -> str: +async def _validate_mermaid(file_path: str, relative_path: str, *, repair: bool = True) -> str: """Run Mermaid validation and return the result string.""" try: - from codewiki.src.be.utils import validate_mermaid_diagrams + from codewiki.src.be.utils import ( + validate_and_repair_mermaid_diagrams, + validate_mermaid_diagrams, + ) - return await validate_mermaid_diagrams(file_path, relative_path) + validator = validate_and_repair_mermaid_diagrams if repair else validate_mermaid_diagrams + return await validator(file_path, relative_path) except Exception as e: - return f"Mermaid validation skipped: {e}" + return f"Warning: Mermaid validation unavailable: {e}" def _save_history(session: SessionState, doc_path: Path, content: str) -> None: @@ -142,7 +146,7 @@ async def handle_edit_doc_file( await asyncio.to_thread(doc_path.write_text, old_content, "utf-8") # Validate Mermaid after undo - mermaid_result = await _validate_mermaid(str(doc_path), filename) + mermaid_result = await _validate_mermaid(str(doc_path), filename, repair=False) return json.dumps( { "status": "undone", @@ -157,7 +161,7 @@ async def handle_edit_doc_file( {"error": f"File not found: {filename}. Use write_doc_file to create it."} ) - current_content = await asyncio.to_thread(doc_path.read_text, "utf-8") + current_content = (await asyncio.to_thread(doc_path.read_bytes)).decode("utf-8") if command == "str_replace": old_str = arguments.get("old_str") @@ -214,6 +218,12 @@ async def handle_edit_doc_file( # Mermaid validation mermaid_result = await _validate_mermaid(str(doc_path), filename) + # Validation may have repaired labels; show the persisted text to the agent. + saved_lines = (await asyncio.to_thread(doc_path.read_text, "utf-8")).split("\n") + snippet = "\n".join( + f"{i + 1:6}\t{saved_lines[i]}" for i in range(start, min(end, len(saved_lines))) + ) + result = { "status": "edited", "command": command, diff --git a/codewiki/src/be/agent_tools/str_replace_editor.py b/codewiki/src/be/agent_tools/str_replace_editor.py index a892e755..2a8a4f8e 100644 --- a/codewiki/src/be/agent_tools/str_replace_editor.py +++ b/codewiki/src/be/agent_tools/str_replace_editor.py @@ -17,7 +17,7 @@ from pydantic import BeforeValidator from pydantic_ai import RunContext, Tool -from ..utils import validate_mermaid_diagrams +from ..utils import validate_and_repair_mermaid_diagrams, validate_mermaid_diagrams from .deps import CodeWikiDeps # Configure logging and monitoring @@ -422,6 +422,7 @@ def __init__(self, REGISTRY, absolute_docs_path=None): self._encoding = None self.REGISTRY = REGISTRY self.logs = [] + self.did_write = False self.absolute_docs_path = Path(absolute_docs_path) if absolute_docs_path else None def _get_display_path(self, path: Path) -> str: @@ -436,11 +437,20 @@ def _get_display_path(self, path: Path) -> str: @property def _file_history(self): - return defaultdict(list, json.loads(self.REGISTRY.get("file_history", "{}"))) + stored = self.REGISTRY.get("file_history", "{}") + stored = json.loads(stored) if isinstance(stored, str) else stored + return defaultdict(list, {Path(key): value for key, value in stored.items()}) @_file_history.setter def _file_history(self, value: dict): - self.REGISTRY["file_history"] = json.dumps(value) + self.REGISTRY["file_history"] = json.dumps( + {str(key): items for key, items in value.items()} + ) + + def _remember(self, path: Path, content: str) -> None: + history = self._file_history + history[path].append(content) + self._file_history = history def __call__( self, @@ -525,8 +535,9 @@ def create_file(self, path: Path, file_text: str): f"The parent directory {self._get_display_path(path.parent)} does not exist. Please create it first." ) return - self.write_file(path, file_text) - self._file_history[path].append(file_text) + if not self.write_file(path, file_text): + return + self._remember(path, file_text) self.logs.append(f"File created successfully at: {self._get_display_path(path)}") def _inside_docs(self, path: Path) -> bool: @@ -630,7 +641,8 @@ def view(self, path: Path, view_range: list[int] | None = None): def str_replace(self, path: Path, old_str: str, new_str: str | None): """Implement the str_replace command, which replaces old_str with new_str in the file content""" # Read the file content - file_content = self.read_file(path).expandtabs() + history_content = self.read_file(path) + file_content = history_content.expandtabs() old_str = old_str.expandtabs() new_str = new_str.expandtabs() if new_str is not None else "" @@ -666,7 +678,8 @@ def str_replace(self, path: Path, old_str: str, new_str: str | None): new_file_content = file_content.replace(old_str, new_str) # Write the new content to the file - self.write_file(path, new_file_content) + if not self.write_file(path, new_file_content): + return post_edit_lint = "" if USE_LINTER: @@ -691,7 +704,7 @@ def str_replace(self, path: Path, old_str: str, new_str: str | None): epilogue = LINT_WARNING_TEMPLATE.format(errors=errors) # Save the content to history - self._file_history[path].append(file_content) + self._remember(path, history_content) # Create a snippet of the edited section replacement_line = file_content.split(old_str)[0].count("\n") @@ -720,7 +733,8 @@ def str_replace(self, path: Path, old_str: str, new_str: str | None): def insert(self, path: Path, insert_line: int, new_str: str): """Implement the insert command, which inserts new_str at the specified line in the file content.""" - file_text = self.read_file(path).expandtabs() + history_content = self.read_file(path) + file_text = history_content.expandtabs() new_str = new_str.expandtabs() file_text_lines = file_text.split("\n") n_lines_file = len(file_text_lines) @@ -744,8 +758,9 @@ def insert(self, path: Path, insert_line: int, new_str: str): new_file_text = "\n".join(new_file_text_lines) snippet = "\n".join(snippet_lines) - self.write_file(path, new_file_text) - self._file_history[path].append(file_text) + if not self.write_file(path, new_file_text): + return + self._remember(path, history_content) # todo: Also expand these windows @@ -760,12 +775,16 @@ def insert(self, path: Path, insert_line: int, new_str: str): def undo_edit(self, path: Path): """Implement the undo_edit command.""" - if not self._file_history[path]: + history = self._file_history + if not history[path]: self.logs.append(f"No edit history found for {self._get_display_path(path)}.") return - old_text = self._file_history[path].pop() - self.write_file(path, old_text) + old_text = history[path][-1] + if not self.write_file(path, old_text): + return + history[path].pop() + self._file_history = history self.logs.append( f"Last edit to {self._get_display_path(path)} undone successfully. {self._make_output(old_text, self._get_display_path(path))}" @@ -782,7 +801,8 @@ def read_file(self, path: Path): exception = None for self._encoding, errors in encodings: try: - text = path.read_text(encoding=self._encoding, errors=errors) + with path.open(encoding=self._encoding, errors=errors, newline="") as stream: + text = stream.read() except UnicodeDecodeError as e: exception = e else: @@ -797,12 +817,15 @@ def read_file(self, path: Path): def write_file(self, path: Path, file: str): """Write the content of a file to a given path; raise a ToolError if an error occurs.""" try: - path.write_text(file, encoding=self._encoding or "utf-8") + with path.open("w", encoding=self._encoding or "utf-8", newline="") as stream: + stream.write(file) + self.did_write = True + return True except Exception as e: # noqa: BLE001 — reported to the agent as a tool message self.logs.append( f"Ran into {e} while trying to write to {self._get_display_path(path)}" ) - return + return False def _make_output( self, @@ -928,8 +951,18 @@ async def str_replace_editor( result = "\n".join(tool.logs) - if command != "view" and path.endswith(".md"): - mermaid_validation = await validate_mermaid_diagrams(absolute_path, path) + if tool.did_write and path.endswith(".md"): + before_validation = Path(absolute_path).read_bytes() + validator = ( + validate_mermaid_diagrams + if command == "undo_edit" + else validate_and_repair_mermaid_diagrams + ) + mermaid_validation = await validator(absolute_path, path) + if Path(absolute_path).read_bytes() != before_validation: + result = "File saved with repaired Mermaid labels.\n" + tool._make_output( + Path(absolute_path).read_text(encoding="utf-8"), path + ) result = result + "\n---------- Mermaid validation ----------\n" + mermaid_validation return result diff --git a/codewiki/src/be/caw_toolkit.py b/codewiki/src/be/caw_toolkit.py index 8ff30f22..9a111713 100644 --- a/codewiki/src/be/caw_toolkit.py +++ b/codewiki/src/be/caw_toolkit.py @@ -143,7 +143,10 @@ async def str_replace_editor( insert_line: int | str | None = None, ) -> str: from codewiki.src.be.agent_tools.str_replace_editor import EditTool - from codewiki.src.be.utils import validate_mermaid_diagrams + from codewiki.src.be.utils import ( + validate_and_repair_mermaid_diagrams, + validate_mermaid_diagrams, + ) # ``Literal`` annotations would be the cleanest way to constrain these, # but ``from __future__ import annotations`` turns them into forward refs @@ -226,8 +229,18 @@ async def str_replace_editor( result = "\n".join(edit_tool.logs) - if command != "view" and path.endswith(".md"): - mermaid_validation = await validate_mermaid_diagrams(absolute_path, path) + if edit_tool.did_write and path.endswith(".md"): + before_validation = Path(absolute_path).read_bytes() + validator = ( + validate_mermaid_diagrams + if command == "undo_edit" + else validate_and_repair_mermaid_diagrams + ) + mermaid_validation = await validator(absolute_path, path) + if Path(absolute_path).read_bytes() != before_validation: + result = "File saved with repaired Mermaid labels.\n" + edit_tool._make_output( + Path(absolute_path).read_text(encoding="utf-8"), path + ) result = result + "\n---------- Mermaid validation ----------\n" + mermaid_validation return result diff --git a/codewiki/src/be/documentation_generator.py b/codewiki/src/be/documentation_generator.py index e645cc8b..3e9f8b0c 100644 --- a/codewiki/src/be/documentation_generator.py +++ b/codewiki/src/be/documentation_generator.py @@ -22,6 +22,7 @@ organize_docs, relative_link, ) +from codewiki.src.be.utils import validate_mermaid_content from codewiki.src.be.module_naming import ( dedupe_module_tree_names, find_missing_module_docs, @@ -403,31 +404,50 @@ async def generate_parent_module_docs( phase = "overview" if not module_path else "parent_modules" try: - with self.usage_ledger.in_phase(phase, target=module_name): - parent_docs = self.backend.complete( - prompt, - system_prompt=format_overview_system_prompt(self.config.get_prompt_addition()), - ) - if not parent_docs: - raise RuntimeError( - f"LLM returned empty content for {module_name} overview " - f"(possible output truncation at max_tokens)" - ) - - # Parse and save parent documentation. Subscription-CLI backends - # (claude-code / codex) sometimes ignore the wrapper and - # return raw markdown; fall back to the response as-is in that case - # rather than crashing with an index error. - if "" in parent_docs and "" in parent_docs: - parent_content = parent_docs.split("")[1].split("")[0].strip() - else: - logger.warning( - f"Overview response for {module_name} missing wrapper; " - f"using raw response as markdown." + request = prompt + for attempt in range(2): + with self.usage_ledger.in_phase(phase, target=module_name): + parent_docs = self.backend.complete( + request, + system_prompt=format_overview_system_prompt( + self.config.get_prompt_addition() + ), + ) + if not parent_docs: + raise RuntimeError( + f"LLM returned empty content for {module_name} overview " + f"(possible output truncation at max_tokens)" + ) + # Subscription backends may return raw Markdown instead of + # the requested wrapper. Validate either form before saving. + if "" in parent_docs and "" in parent_docs: + parent_content = ( + parent_docs.split("")[1].split("")[0].strip() + ) + else: + parent_content = parent_docs.strip() + report = await validate_mermaid_content(parent_content, repair=True) + if not report.errors: + if report.unavailable: + logger.warning(report.format(parent_docs_path)) + elif report.repairs: + logger.info(report.format(parent_docs_path)) + break + if attempt == 1: + raise RuntimeError( + f"Invalid Mermaid in {module_name} overview after one correction attempt:\n" + + report.format(parent_docs_path) + ) + request = ( + prompt + + "\n\nCorrect the Mermaid syntax in this previous draft. Return the complete " + "overview, preserving the other content.\n\n" + + report.content + + "\n\n" + + report.format(parent_docs_path) ) - parent_content = parent_docs.strip() os.makedirs(os.path.dirname(parent_docs_path), exist_ok=True) - file_manager.save_text(parent_content, parent_docs_path) + file_manager.save_text(report.content, parent_docs_path) logger.debug(f"Successfully generated parent documentation for: {module_name}") return module_tree diff --git a/codewiki/src/be/mermaid_support.py b/codewiki/src/be/mermaid_support.py new file mode 100644 index 00000000..5e9a0cb9 --- /dev/null +++ b/codewiki/src/be/mermaid_support.py @@ -0,0 +1,188 @@ +"""Shared Mermaid guidance and conservative, parser-independent repair helpers.""" + +from dataclasses import dataclass, field +import re +from typing import Literal + + +MERMAID_GUIDELINES = """ +- Use simple alphanumeric node IDs. Quote flowchart node labels, keeping the shape: + BAD: C{msg.error()?} GOOD: C{"msg.error()?"} + BAD: D[config[key]] GOOD: D["config[key]"] +- In sequence messages, function calls, () {} [], commas and -- are ordinary text: + A->>B: call process(data), config[key], {value} -- result + A literal semicolon must be encoded as #59; (backticks do NOT escape it): + BAD: A->>B: first; second GOOD: A->>B: first#59; second +- Double dashes are valid inside node labels, pipe-delimited edge labels and + sequence messages. Quote inline flowchart edge text that contains --: + BAD: A -- start -- stop --> B GOOD: A -- "start -- stop" --> B + Also valid: A -->|start -- stop| B +- Fix confirmed syntax errors using the returned diagnostics. Validation + unavailable is a warning, not a request to rewrite a diagram. If a tool repairs + a label, use its reported saved replacement in subsequent edits. +""" + + +@dataclass(frozen=True) +class DiagramResult: + status: Literal["valid", "invalid", "unavailable"] + message: str = "" + + +@dataclass +class MermaidReport: + content: str + diagrams: int = 0 + errors: list[str] = field(default_factory=list) + unavailable: list[str] = field(default_factory=list) + repairs: list[str] = field(default_factory=list) + + def format(self, relative_path: str) -> str: + parts = [] + if self.errors: + parts.append( + f"Mermaid syntax errors found in file: {relative_path}\n" + "\n".join(self.errors) + ) + parts.append(MERMAID_GUIDELINES) + if self.unavailable: + parts.append("Warning: Mermaid validation unavailable:\n" + "\n".join(self.unavailable)) + if self.repairs: + parts.append("Mermaid labels repaired and validated:\n" + "\n".join(self.repairs)) + if not self.errors and not self.unavailable: + parts.append( + f"All mermaid diagrams in file: {relative_path} are syntax correct" + if self.diagrams + else "No mermaid diagrams found in the file" + ) + return "\n".join(parts) + + +@dataclass(frozen=True) +class MermaidBlock: + line: int # Opening fence, one-based; parser line 1 is the following line. + start: int + end: int + content: str + closed: bool + + +def mermaid_blocks(content: str) -> list[MermaidBlock]: + """Locate complete fenced blocks without touching Markdown outside their bodies.""" + lines = content.splitlines(keepends=True) + blocks = [] + offset = 0 + i = 0 + while i < len(lines): + opening = re.match(r"^ {0,3}(`{3,}|~{3,})([^\r\n]*)", lines[i]) + if not opening: + offset += len(lines[i]) + i += 1 + continue + fence, info = opening.groups() + opening_line = i + 1 + offset += len(lines[i]) + start = offset + i += 1 + closing = re.compile(r"^ {0,3}" + re.escape(fence[0]) + "{" + str(len(fence)) + r",}\s*$") + while i < len(lines) and not closing.fullmatch(lines[i]): + offset += len(lines[i]) + i += 1 + if info.strip().split()[:1] == ["mermaid"]: + blocks.append( + MermaidBlock(opening_line, start, offset, content[start:offset], i < len(lines)) + ) + if i < len(lines): + offset += len(lines[i]) + i += 1 + return blocks + + +def is_parse_error(message: str) -> bool: + return bool( + re.search(r"parse error|lexical error|syntax error|no diagram type detected", message, re.I) + ) + + +def quote_flowchart_labels(source: str) -> tuple[str, list[tuple[str, str]]]: + """Quote only unambiguous classic rectangle/diamond labels with delimiters. + + This is deliberately not a general Mermaid normalizer. Walk node and label + tokens rather than replacing bracket expressions in messages or edge text. + Unsupported shapes or statements are skipped as whole lines. + """ + if not re.match(r"\s*(?:flowchart|graph)\s+(?:TD|TB|BT|LR|RL)\b", source): + return source, [] + changes = [] + output = [] + for line in source.splitlines(keepends=True): + # Definitions, directives, comments, compound shapes, and inline edge + # labels need their own grammars. Never guess at their contents. + if re.match( + r"\s*(?:%%|---|subgraph\b|end\b|class\b|classDef\b|style\b|linkStyle\b|click\b)", + line, + ) or re.search(r"\[\[|\[\(|\[\||\[/|\[\\|\{\{|@\{", line): + output.append(line) + continue + i = 0 + pieces = [] + first_change = len(changes) + while i < len(line): + if line.startswith("%%", i): + pieces = [line] + del changes[first_change:] + break + # An edge label cannot contain a node definition. + if line[i] in '|"': + end = line.find(line[i], i + 1) + end = len(line) - 1 if end < 0 else end + pieces.append(line[i : end + 1]) + i = end + 1 + continue + if re.match(r"[A-Za-z][A-Za-z0-9_]*(?:\(|>)", line[i:]) or re.match( + r"(?:--|==|-\.)\s+[^>|.\s]", line[i:] + ): + pieces = [line] + del changes[first_change:] + break + node = re.match(r"[A-Za-z][A-Za-z0-9_]*([\[{])", line[i:]) + if not node or (i and (line[i - 1].isalnum() or line[i - 1] in "_@")): + pieces.append(line[i]) + i += 1 + continue + label_start = i + len(node.group()) + delimiter = node.group(1) + close = "]" if delimiter == "[" else "}" + j = label_start + depth = 1 + quoted = False + while j < len(line) and line[j] not in "\r\n": + char = line[j] + if char == '"': + quoted = not quoted + if not quoted: + if char == delimiter: + depth += 1 + elif char == close: + depth -= 1 + if depth == 0: + break + j += 1 + if depth: + # An unmatched token makes the rest of this line ambiguous. + pieces.append(line[i:]) + break + label = line[label_start:j] + original = line[i : j + 1] + if ( + re.search(r"[()\[\]{}]", label) + and not any(c in label for c in '"`<>|\\') + and label.strip() + ): + replacement = line[i:label_start] + '"' + label + '"' + close + pieces.append(replacement) + changes.append((original, replacement)) + else: + pieces.append(original) + i = j + 1 + output.append("".join(pieces)) + return "".join(output), changes diff --git a/codewiki/src/be/prompt_template.py b/codewiki/src/be/prompt_template.py index c483fa2b..fd274261 100644 --- a/codewiki/src/be/prompt_template.py +++ b/codewiki/src/be/prompt_template.py @@ -3,6 +3,7 @@ from typing import Any from codewiki.src.be.doc_layout import doc_relpaths, module_doc_file +from codewiki.src.be.mermaid_support import MERMAID_GUIDELINES from codewiki.src.config import LAYOUT_FLAT from codewiki.src.utils import file_manager @@ -140,6 +141,14 @@ """.strip() +# These templates are formatted later; braces in literal Mermaid labels must +# survive that formatting exactly once. +_MERMAID_TEMPLATE_GUIDANCE = MERMAID_GUIDELINES.replace("{", "{{").replace("}", "}}") +SYSTEM_PROMPT += "\n\n" + _MERMAID_TEMPLATE_GUIDANCE +LEAF_SYSTEM_PROMPT += "\n\n" + _MERMAID_TEMPLATE_GUIDANCE +REPO_OVERVIEW_PROMPT += "\n\n" + _MERMAID_TEMPLATE_GUIDANCE +MODULE_OVERVIEW_PROMPT += "\n\n" + _MERMAID_TEMPLATE_GUIDANCE + CLUSTER_REPO_PROMPT = """ Here is list of all potential core components of the repository (It's normal that some components are not essential to the repository): diff --git a/codewiki/src/be/updater/prompts.py b/codewiki/src/be/updater/prompts.py index 18dacc4b..7eafaab6 100644 --- a/codewiki/src/be/updater/prompts.py +++ b/codewiki/src/be/updater/prompts.py @@ -11,6 +11,7 @@ from typing import Any from codewiki.src.be.dependency_analyzer.models.core import Node +from codewiki.src.be.mermaid_support import MERMAID_GUIDELINES from codewiki.src.be.prompt_template import _fence_language, _format_module_tree_str from codewiki.src.be.updater.change_report import LeafReport from codewiki.src.be.updater.graph_diff import GraphDiff, documented_source @@ -62,6 +63,8 @@ {custom_instructions} """.strip() +UPDATE_LEAF_SYSTEM_PROMPT += "\n\n" + MERMAID_GUIDELINES.replace("{", "{{").replace("}", "}}") + UPDATE_LEAF_USER_PROMPT = """ Update the documentation for the module `{leaf_name}` (mode: {mode}). @@ -175,6 +178,8 @@ "reason": "..."}}}. """.strip() +STALE_FIX_SYSTEM_PROMPT += "\n\n" + MERMAID_GUIDELINES + STALE_FIX_USER_PROMPT = """ Page to fix: `{page_path}` (view it with str_replace_editor, working_dir="docs"). diff --git a/codewiki/src/be/utils.py b/codewiki/src/be/utils.py index bf0571de..0b444aa5 100644 --- a/codewiki/src/be/utils.py +++ b/codewiki/src/be/utils.py @@ -6,6 +6,14 @@ from pathlib import Path from typing import List, Tuple import logging + +from codewiki.src.be.mermaid_support import ( + DiagramResult, + MermaidReport, + is_parse_error, + mermaid_blocks, + quote_flowchart_labels, +) import tiktoken @@ -68,88 +76,92 @@ def count_tokens(text: str) -> int: # ------------------------------------------------------------ -async def validate_mermaid_diagrams(md_file_path: str, relative_path: str) -> str: - """ - Validate all Mermaid diagrams in a markdown file. - - Args: - md_file_path: Path to the markdown file to check - relative_path: Relative path to the markdown file - Returns: - "All mermaid diagrams are syntax correct" if all diagrams are valid, - otherwise returns error message with details about invalid diagrams - """ +def _diagram_message(message: str, diagram_num: int, line_start: int) -> str: + # Translate parser-relative line numbers without dropping useful error text. + message = re.sub( + r"line (\d+)", + lambda match: f"line {line_start + int(match.group(1))}", + message, + ) + return f"Diagram {diagram_num}: {message}" + +async def validate_mermaid_content(content: str, *, repair: bool = False) -> MermaidReport: + """Validate Markdown and optionally return only parser-confirmed repairs.""" + report = MermaidReport(content) + replacements = [] + for number, block in enumerate(mermaid_blocks(content), 1): + report.diagrams += 1 + result = await _validate_diagram(block.content) + if ( + repair + and block.closed + and result.status == "invalid" + and re.search( + r"got ['\"]?(?:PS|PE|SQS|SQE|DIAMOND_START|DIAMOND_STOP)\b", result.message + ) + ): + candidate, changes = quote_flowchart_labels(block.content) + if changes: + checked = await _validate_diagram(candidate) + if checked.status == "valid": + replacements.append((block.start, block.end, candidate)) + report.repairs.extend( + f"Diagram {number} (fence line {block.line}): {old} -> {new}" + for old, new in changes + ) + result = checked + elif checked.status == "unavailable": + report.unavailable.append( + _diagram_message( + "repair not saved; revalidation unavailable: " + checked.message, + number, + block.line, + ) + ) + if result.status == "invalid": + report.errors.append(_diagram_message(result.message, number, block.line)) + elif result.status == "unavailable": + report.unavailable.append(_diagram_message(result.message, number, block.line)) + for start, end, replacement in reversed(replacements): + report.content = report.content[:start] + replacement + report.content[end:] + return report + + +async def _validate_mermaid_file(md_file_path: str, relative_path: str, *, repair: bool) -> str: try: - # Read the markdown file file_path = Path(md_file_path) if not file_path.exists(): return f"Error: File '{md_file_path}' does not exist" - - content = file_path.read_text(encoding="utf-8") - - # Extract all mermaid code blocks - mermaid_blocks = extract_mermaid_blocks(content) - - if not mermaid_blocks: - return "No mermaid diagrams found in the file" - - # Validate each mermaid diagram sequentially to avoid segfaults - errors = [] - for i, (line_start, diagram_content) in enumerate(mermaid_blocks, 1): - error_msg = await validate_single_diagram(diagram_content, i, line_start) - if error_msg: - errors.append("\n") - errors.append(error_msg) - - # if errors: - # logger.debug(f"Mermaid syntax errors found in file: {md_file_path}: {errors}") - - if errors: - return ( - "Mermaid syntax errors found in file: " + relative_path + "\n" + "\n".join(errors) - ) - else: - return "All mermaid diagrams in file: " + relative_path + " are syntax correct" - + original = file_path.read_bytes() + report = await validate_mermaid_content(original.decode("utf-8"), repair=repair) + if report.content.encode("utf-8") != original: + # Validation may await a remote renderer. Never overwrite an edit + # made while it was running, or claim an unsaved repair succeeded. + if file_path.read_bytes() != original: + return "Warning: File changed during Mermaid validation; re-run validation." + file_path.write_bytes(report.content.encode("utf-8")) + return report.format(relative_path) except Exception as e: - return f"Error processing file: {str(e)}" - - -def extract_mermaid_blocks(content: str) -> List[Tuple[int, str]]: - """ - Extract all mermaid code blocks from markdown content. - - Returns: - List of tuples containing (line_number, diagram_content) - """ - mermaid_blocks = [] - lines = content.split("\n") - i = 0 + return f"Warning: Mermaid validation unavailable for {relative_path}: {e}" - while i < len(lines): - line = lines[i].strip() - # Look for mermaid code block start - if line == "```mermaid" or line.startswith("```mermaid"): - start_line = i + 1 - diagram_lines = [] - i += 1 +async def validate_mermaid_diagrams(md_file_path: str, relative_path: str) -> str: + """Validate without changing the file, including after an undo operation.""" + return await _validate_mermaid_file(md_file_path, relative_path, repair=False) - # Collect lines until we find the closing ``` - while i < len(lines): - if lines[i].strip() == "```": - break - diagram_lines.append(lines[i]) - i += 1 - if diagram_lines: # Only add non-empty diagrams - diagram_content = "\n".join(diagram_lines) - mermaid_blocks.append((start_line, diagram_content)) +async def validate_and_repair_mermaid_diagrams(md_file_path: str, relative_path: str) -> str: + """Validate a successful write and persist only verified flowchart repairs.""" + return await _validate_mermaid_file(md_file_path, relative_path, repair=True) - i += 1 - return mermaid_blocks +def extract_mermaid_blocks(content: str) -> List[Tuple[int, str]]: + """Return opening-fence line numbers and bodies (legacy read-only API).""" + return [ + (block.line, block.content.removesuffix("\n").removesuffix("\r")) + for block in mermaid_blocks(content) + ] # PythonMonkey 1.3.1 only supports Python 3.8–3.11. On 3.12+ it still imports, @@ -210,7 +222,7 @@ async def _try_pythonmonkey_parse(diagram_content: str) -> str | None: _PYTHONMONKEY_BROKEN = True return None match = re.search(r"Error:(.*?)(?=Stack Trace:|$)", error_str, re.DOTALL) - if match: + if match and is_parse_error(match.group(0)): return match.group(0).strip() # Unknown error from the JS parser — fall back rather than surface it. return None @@ -219,20 +231,41 @@ async def _try_pythonmonkey_parse(diagram_content: str) -> str | None: sys.stderr = old_stderr -def _parse_via_mermaid_py(diagram_content: str) -> str: - """Validate via mermaid-py. Returns parse-error text, or "" if valid. +def _remote_result(status_code: int, body: str) -> DiagramResult: + if 200 <= status_code < 300: + return DiagramResult("valid") + if status_code in (400, 422) and is_parse_error(body): + # Newer mermaid-py truncates str(MermaidError), losing the final + # lexer token. Use the response body for actionable diagnostics. + return DiagramResult("invalid", body) + return DiagramResult("unavailable", f"HTTP {status_code}: {body[:500]}") + - mermaid-py raises MermaidError on parse failure and returns an SVG body - on success — we must drive the result off the exception, not the body - text, otherwise a successful SVG gets reported as a parse error. +def _parse_via_mermaid_py(diagram_content: str) -> DiagramResult: + """Validate HTTP responses explicitly, including mermaid-py 0.8.0. + + 0.8.0 returns error responses without raising. Later versions raise + MermaidError; both must distinguish service failures from parse errors. """ import mermaid as md try: - md.Mermaid(diagram_content) - return "" + diagram = md.Mermaid(diagram_content) except Exception as e: - return str(e) + if isinstance(e, getattr(md, "MermaidError", ())): + status_code = getattr(e, "status_code", None) + if isinstance(status_code, int): + return _remote_result(status_code, getattr(e, "response_text", str(e))) + return DiagramResult("unavailable", str(e)) + for name in ("svg_response", "img_response"): + response = getattr(diagram, name, None) + status_code = getattr(response, "status_code", None) + if not isinstance(status_code, int): + return DiagramResult("unavailable", "renderer returned no HTTP response") + result = _remote_result(status_code, response.text) + if result.status != "valid": + return result + return DiagramResult("valid") def _run_in_daemon_thread(fn, *args) -> "asyncio.Future": @@ -264,63 +297,34 @@ def _worker(): return fut -async def validate_single_diagram(diagram_content: str, diagram_num: int, line_start: int) -> str: - """ - Validate a single mermaid diagram. - - Args: - diagram_content: The mermaid diagram content - diagram_num: Diagram number for error reporting - line_start: Starting line number in the file - - Returns: - Error message if invalid, empty string if valid - """ +async def _validate_diagram(diagram_content: str) -> DiagramResult: global _MERMAID_PY_BROKEN core_error = await _try_pythonmonkey_parse(diagram_content) - if core_error is None: - if _MERMAID_PY_BROKEN: - # Validation disabled/unavailable is not a syntax error — a - # non-empty return here would make callers report valid diagrams - # as broken and send agents into fix loops. - logger.debug( - "Diagram %d: validation skipped (mermaid-py disabled or unavailable)", diagram_num - ) - return "" - try: - core_error = await asyncio.wait_for( - _run_in_daemon_thread(_parse_via_mermaid_py, diagram_content), - timeout=15.0, - ) - except asyncio.TimeoutError: - # Inconclusive, not a parse failure. Latch so the remaining - # diagrams (and later calls) don't each block 15s on the same - # unreachable rendering service. - _MERMAID_PY_BROKEN = True - logger.warning( - "Diagram %d: mermaid validation timed out (15s); skipping further validation", - diagram_num, - ) - return "" - except Exception as e: - return f" Diagram {diagram_num}: Exception during validation - {str(e)}" - - if not core_error: - return "" - - line_match = re.search(r"line (\d+)", core_error) - if line_match: - error_line_in_diagram = int(line_match.group(1)) - actual_line_in_file = line_start + error_line_in_diagram - newline = "\n" - return f"Diagram {diagram_num}: Parse error on line {actual_line_in_file}:{newline}{newline.join(core_error.split(newline)[1:])}" - return f"Diagram {diagram_num}: {core_error}" - + if core_error is not None: + if not core_error: + return DiagramResult("valid") + if is_parse_error(core_error): + return DiagramResult("invalid", core_error) + if _MERMAID_PY_BROKEN: + return DiagramResult("unavailable", "remote validation disabled or previously timed out") + try: + return await asyncio.wait_for( + _run_in_daemon_thread(_parse_via_mermaid_py, diagram_content), timeout=15.0 + ) + except asyncio.TimeoutError: + _MERMAID_PY_BROKEN = True + logger.warning("Mermaid validation timed out (15s); skipping further remote validation") + return DiagramResult("unavailable", "remote renderer timed out after 15s") + except Exception as e: + return DiagramResult("unavailable", str(e)) -if __name__ == "__main__": - # Test with the provided file - import asyncio - test_file = "output/docs/SWE_agent-docs/agent_hooks.md" - result = asyncio.run(validate_mermaid_diagrams(test_file, "agent_hooks.md")) - print(result) +async def validate_single_diagram(diagram_content: str, diagram_num: int, line_start: int) -> str: + """Legacy string API: empty only on success; unavailable is an explicit warning.""" + result = await _validate_diagram(diagram_content) + if result.status == "valid": + return "" + message = _diagram_message(result.message, diagram_num, line_start) + if result.status == "unavailable": + return "Warning: Mermaid validation unavailable: " + message + return message diff --git a/skills/codewiki-wiki-generator/SKILL.md b/skills/codewiki-wiki-generator/SKILL.md index 0aae9092..55db977d 100644 --- a/skills/codewiki-wiki-generator/SKILL.md +++ b/skills/codewiki-wiki-generator/SKILL.md @@ -165,16 +165,22 @@ The granularity of incremental updates is **module-level** — if any component ## Mermaid Syntax Guidelines -```mermaid -graph TD - A[ComponentA] --> B[ComponentB] - A --> C[ComponentC] -``` - -- Node IDs use only letters and digits (avoid Chinese characters, spaces, colons) -- Node labels wrapped in square brackets: `A[display text]` -- Subgraph syntax: `subgraph title ... end` -- Do not use interactive syntax like `click`, `linkStyle`, etc. +- Use simple alphanumeric node IDs and quote flowchart labels, keeping the shape: + - BAD: `C{msg.error()?}` → GOOD: `C{"msg.error()?"}` + - BAD: `D[config[key]]` → GOOD: `D["config[key]"]` +- In sequence messages, function calls, `()` `{}` `[]`, commas and `--` are ordinary text: + `A->>B: call process(data), config[key], {value} -- result` +- A literal sequence-message semicolon must be encoded as `#59;`. Backticks do **not** escape it: + - BAD: `A->>B: first; second` → GOOD: `A->>B: first#59; second` +- Double dashes are valid inside node labels, pipe-delimited edge labels and sequence messages. + Quote inline flowchart edge text containing `--`: + - BAD: `A -- start -- stop --> B` → GOOD: `A -- "start -- stop" --> B` + - Also valid: `A -->|start -- stop| B`. Do not rewrite valid `--` text or command-line flags. +- Subgraph syntax: `subgraph title ... end`. +- Do not use interactive syntax such as `click`. +- Fix confirmed syntax errors using the returned diagnostics. Validation unavailable is a warning, + not a request to rewrite a diagram. If a tool repairs a label, use its reported saved replacement + in subsequent edits. ## Error Handling diff --git a/tests/mermaid/check.mjs b/tests/mermaid/check.mjs new file mode 100644 index 00000000..86c7543a --- /dev/null +++ b/tests/mermaid/check.mjs @@ -0,0 +1,24 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { JSDOM } from 'jsdom'; + +const dom = new JSDOM(''); +globalThis.window = dom.window; +globalThis.document = dom.window.document; +globalThis.Element = dom.window.Element; +const { default: mermaid } = await import('mermaid'); +mermaid.initialize({ startOnLoad: false, securityLevel: 'strict' }); +const fixtures = JSON.parse(readFileSync(new URL('./fixtures.json', import.meta.url))); +for (const fixture of fixtures) { + let valid = true; + try { + await mermaid.parse(fixture.source); + } catch { + valid = false; + } + assert.equal(valid, fixture.valid, fixture.name); + if (fixture.repaired) { + assert.ok(await mermaid.parse(fixture.repaired), `${fixture.name}: repaired`); + } +} +console.log(`${fixtures.length} Mermaid 11.9.0 parser fixtures passed`); diff --git a/tests/mermaid/fixtures.json b/tests/mermaid/fixtures.json new file mode 100644 index 00000000..04531b57 --- /dev/null +++ b/tests/mermaid/fixtures.json @@ -0,0 +1,158 @@ +[ + { + "name": "decision function", + "source": "flowchart TD\nC{msg.error()?}", + "valid": false, + "repaired": "flowchart TD\nC{\"msg.error()?\"}" + }, + { + "name": "rectangle function", + "source": "graph LR\nA[consumer.poll(1.0)] --> B[End]", + "valid": false, + "repaired": "graph LR\nA[\"consumer.poll(1.0)\"] --> B[End]" + }, + { + "name": "nested brackets", + "source": "flowchart TD\nD[config[key]]", + "valid": false, + "repaired": "flowchart TD\nD[\"config[key]\"]" + }, + { + "name": "nested braces", + "source": "flowchart TD\nD{config{key}}", + "valid": false, + "repaired": "flowchart TD\nD{\"config{key}\"}" + }, + { + "name": "multiple nodes", + "source": "flowchart TD\nA[process(data)] --> B{msg.error()?}", + "valid": false, + "repaired": "flowchart TD\nA[\"process(data)\"] --> B{\"msg.error()?\"}" + }, + { + "name": "quoted decision", + "source": "flowchart TD\nC{\"msg.error()?\"}", + "valid": true + }, + { + "name": "plain labels", + "source": "flowchart TD\nA[Start] --> B{Ready?}", + "valid": true + }, + { + "name": "sequence call", + "source": "sequenceDiagram\nA->>B: call process(data)", + "valid": true + }, + { + "name": "sequence punctuation", + "source": "sequenceDiagram\nA->>B: process(a, b), config[key], {value}", + "valid": true + }, + { + "name": "sequence double dash", + "source": "sequenceDiagram\nA->>B: start -- stop", + "valid": true + }, + { + "name": "sequence raw semicolon", + "source": "sequenceDiagram\nA->>B: first; second", + "valid": false + }, + { + "name": "sequence backtick semicolon", + "source": "sequenceDiagram\nA->>B: `first; second`", + "valid": false + }, + { + "name": "sequence encoded semicolon", + "source": "sequenceDiagram\nA->>B: first#59; second", + "valid": true + }, + { + "name": "pipe double dash", + "source": "flowchart TD\nA -->|start -- stop| B", + "valid": true + }, + { + "name": "inline double dash", + "source": "flowchart TD\nA -- start -- stop --> B", + "valid": false + }, + { + "name": "quoted inline double dash", + "source": "flowchart TD\nA -- \"start -- stop\" --> B", + "valid": true + }, + { + "name": "node command flag", + "source": "flowchart TD\nA[run --dry-run]", + "valid": true + }, + { + "name": "pipe function", + "source": "flowchart TD\nA -->|process(data)| B", + "valid": false + }, + { + "name": "compound shape", + "source": "flowchart TD\nA[[\"func(data)\"]] --> B[(\"store(key)\")]", + "valid": true + }, + { + "name": "round shape", + "source": "flowchart TD\nA(\"func(data)\") --> B((\"func(data)\"))", + "valid": true + }, + { + "name": "html label", + "source": "flowchart TD\nA[\"func()\"]", + "valid": true + }, + { + "name": "raw html label", + "source": "flowchart TD\nA[func()]", + "valid": false + }, + { + "name": "markdown label", + "source": "flowchart TD\nA[\"`func(data)`\"]", + "valid": true + }, + { + "name": "comment", + "source": "flowchart TD\n%% C{msg.error()?}\nA --> B", + "valid": true + }, + { + "name": "unbalanced label", + "source": "flowchart TD\nA[config[key]", + "valid": false + }, + { + "name": "style statement", + "source": "flowchart TD\nA --> B\nclassDef green fill:#0f0;\nclass A green", + "valid": true + }, + { + "name": "compound alongside repairable", + "source": "flowchart TD\nA[[foo]] --> C{msg.error()?}", + "valid": false + }, + { + "name": "inline comment untouched", + "source": "flowchart TD\nA[process(data)] %% C{msg.error()?}", + "valid": false + }, + { + "name": "dash in label alongside function", + "source": "flowchart TD\nA[process(data) -- result]", + "valid": false, + "repaired": "flowchart TD\nA[\"process(data) -- result\"]" + }, + { + "name": "thick inline edge text untouched", + "source": "flowchart TD\nA == process(data) ==> B", + "valid": true + } +] diff --git a/tests/mermaid/package-lock.json b/tests/mermaid/package-lock.json new file mode 100644 index 00000000..3a84cbb8 --- /dev/null +++ b/tests/mermaid/package-lock.json @@ -0,0 +1,2011 @@ +{ + "name": "codewiki-mermaid-regressions", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "codewiki-mermaid-regressions", + "devDependencies": { + "jsdom": "26.1.0", + "mermaid": "11.9.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", + "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/cst-dts-gen/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.0.0", + "@antfu/utils": "^8.1.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.0", + "globals": "^15.14.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "mlly": "^1.7.4" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.3.tgz", + "integrity": "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA==", + "dev": true, + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz", + "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz", + "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/chevrotain/node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/confbox": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.3.1.tgz", + "integrity": "sha512-cKUSoKa8YxFZZSmraVi7onONx3amu77ngK3kGpsYHDH7drPwCRkQE1RYMPlLRrMtnciRj274XNRxcHxnKmDSnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dev": true, + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cytoscape": { + "version": "3.34.4", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.4.tgz", + "integrity": "sha512-msY99tMY8iWTg+sIPGfSqDcPNOt4xcj+v/K/7qUFaCUxxlqnMkUYLBjRUzn9W5CU/R2hrmtJa5+UtovrEM2ctg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "dev": true, + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", + "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", + "dev": true, + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/dompurify": { + "version": "3.4.17", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.17.tgz", + "integrity": "sha512-B/pSlYVHQ58kqISD/yDjuJ1yfzI34m1gF4lAod2bwSRhiN4K5Cl3H8Af7hqB2PmTL911tdEudnnXn5FnhRoz0w==", + "dev": true, + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/exsolve": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.2.0.tgz", + "integrity": "sha512-h1pQPR9XoeSMdntRNCjyTznaume4UpfXrULBnlkZ2CUM/G5eSa+pBByGldO5uR8dxbPC+OuJXsPPNf+JZ81ADg==", + "dev": true, + "license": "MIT" + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "dev": true + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "dev": true, + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.2.1.tgz", + "integrity": "sha512-++gUqRDEvcnN6Zhqrr+y/CkVEHhlrR96vZn3nZZPYzMcBUyBtTKzB9NadClFIsIVSsu+3i9tfk/erqy9kAmt7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mermaid": { + "version": "11.9.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.9.0.tgz", + "integrity": "sha512-YdPXn9slEwO0omQfQIsW6vS84weVQftIyyTGAZCwM//MGhPzL1+l6vO6bkf0wnP4tHigH1alZ5Ooy3HXI2gOag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.0.4", + "@iconify/utils": "^2.1.33", + "@mermaid-js/parser": "^0.6.2", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.11", + "dayjs": "^1.11.13", + "dompurify": "^3.2.5", + "katex": "^0.16.22", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^16.0.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nwsapi": { + "version": "2.2.28", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.28.tgz", + "integrity": "sha512-IlVB7OS7qrOsVYlpnFIkETjMwT9jwvmocJmmM+GZU/PAB3uGi9Ezd7vcWhWBUnSc0ya4ppmQITOyP1ez9gg8cg==", + "dev": true, + "license": "MIT" + }, + "node_modules/package-manager-detector": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pkg-types": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.4.0.tgz", + "integrity": "sha512-07Fys7iVx0X6TcviMOgogM1Lk33ZyqOezMnU79S+kffWqwolG/5W9V8EgQSeYCIfFTYVgssRbnH630Oa2+soBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.3.1", + "exsolve": "^1.1.3", + "pathe": "^2.0.3" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "dev": true, + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.16.tgz", + "integrity": "sha512-qeNEuOi9a3qkqRf7Gfez2WL2VwlBTLCNekHBGO9kxPVUp9XuY15yfTJ9jwvOwKI0I2MhUUfZq95NSp+LK1CLQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/tests/mermaid/package.json b/tests/mermaid/package.json new file mode 100644 index 00000000..ca4a801d --- /dev/null +++ b/tests/mermaid/package.json @@ -0,0 +1,12 @@ +{ + "name": "codewiki-mermaid-regressions", + "private": true, + "type": "module", + "scripts": { + "test": "node check.mjs" + }, + "devDependencies": { + "mermaid": "11.9.0", + "jsdom": "26.1.0" + } +} diff --git a/tests/test_mermaid_validation.py b/tests/test_mermaid_validation.py new file mode 100644 index 00000000..da09198c --- /dev/null +++ b/tests/test_mermaid_validation.py @@ -0,0 +1,293 @@ +"""Mermaid syntax, repair persistence and inconclusive validation (issue #141).""" + +import asyncio +import json +from pathlib import Path + +import pytest + +from codewiki.src.be import utils +from codewiki.src.be.mermaid_support import ( + DiagramResult, + MERMAID_GUIDELINES, + mermaid_blocks, + quote_flowchart_labels, +) + +FIXTURES = json.loads((Path(__file__).parent / "mermaid/fixtures.json").read_text()) +INVALID = "flowchart TD\nC{msg.error()?}" +FIXED = 'flowchart TD\nC{"msg.error()?"}' +PARSE_ERROR = "Error: Parse error on line 2: expected DIAMOND_STOP, got 'PS'" + + +def markdown(diagram=INVALID): + return f"```mermaid\n{diagram}\n```\n" + + +@pytest.fixture +def parser(monkeypatch): + calls = [] + + async def parse(source): + calls.append(source) + if INVALID in source.replace("\r\n", "\n"): + return DiagramResult("invalid", PARSE_ERROR) + if "broken" in source: + return DiagramResult("invalid", "Parse error on line 2: got 'TEXT'") + return DiagramResult("valid") + + monkeypatch.setattr(utils, "_validate_diagram", parse) + return calls + + +@pytest.mark.parametrize("case", FIXTURES, ids=lambda case: case["name"]) +def test_repair_fixtures_match_real_parser_expectations(case): + repaired, changes = quote_flowchart_labels(case["source"]) + assert repaired == case.get("repaired", case["source"]) + assert bool(changes) == ("repaired" in case) + assert quote_flowchart_labels(repaired) == (repaired, []) + + +def test_read_only_validation_reports_absolute_lines(parser): + async def check(): + original = markdown() + report = await utils.validate_mermaid_content(original, repair=False) + assert report.content == original + assert report.errors and not report.repairs + assert "line 3" in report.errors[0] + assert "got" in report.errors[0] + assert MERMAID_GUIDELINES in report.format("x.md") + + asyncio.run(check()) + assert len(parser) == 1 + + +def test_repairs_only_after_failure_and_persists_exact_bytes(tmp_path, parser): + path = tmp_path / "x.md" + before = ( + ("# Intro\r\n\r\n" + markdown() + "\n```python\nC{msg.error()?}\n```\n") + .replace("\n", "\r\n") + .replace("\r\r\n", "\r\n") + .encode() + ) + path.write_bytes(before) + readonly = asyncio.run(utils.validate_mermaid_diagrams(str(path), "x.md")) + assert "syntax errors" in readonly + assert path.read_bytes() == before + result = asyncio.run(utils.validate_and_repair_mermaid_diagrams(str(path), "x.md")) + assert "repaired and validated" in result + assert "fence line 3" in result + assert path.read_bytes() == before.replace( + INVALID.replace("\n", "\r\n").encode(), FIXED.replace("\n", "\r\n").encode(), 1 + ) + after = path.read_bytes() + result = asyncio.run(utils.validate_and_repair_mermaid_diagrams(str(path), "x.md")) + assert "syntax correct" in result and "repaired" not in result + assert path.read_bytes() == after + + +@pytest.mark.parametrize("status", ["invalid", "unavailable"]) +def test_failed_revalidation_does_not_save_candidate(tmp_path, monkeypatch, status): + path = tmp_path / "x.md" + path.write_text(markdown()) + original = path.read_bytes() + results = iter([DiagramResult("invalid", PARSE_ERROR), DiagramResult(status, "still broken")]) + + async def parse(_): + return next(results) + + monkeypatch.setattr(utils, "_validate_diagram", parse) + result = asyncio.run(utils.validate_and_repair_mermaid_diagrams(str(path), "x.md")) + assert "syntax errors" in result and "repaired and validated" not in result + assert path.read_bytes() == original + if status == "unavailable": + assert "revalidation unavailable" in result + + +def test_concurrent_change_is_not_overwritten(tmp_path, monkeypatch): + path = tmp_path / "x.md" + path.write_text(markdown()) + + async def parse(source): + if INVALID in source.replace("\r\n", "\n"): + return DiagramResult("invalid", PARSE_ERROR) + path.write_text("concurrent edit") + return DiagramResult("valid") + + monkeypatch.setattr(utils, "_validate_diagram", parse) + result = asyncio.run(utils.validate_and_repair_mermaid_diagrams(str(path), "x.md")) + assert "File changed" in result and "repaired and validated" not in result + assert path.read_text() == "concurrent edit" + + +def test_multiple_diagrams_and_non_mermaid_fences(parser): + original = ( + "# Title\n````markdown\n" + + markdown() + + "````\n" + + markdown() + + "\n~~~mermaid\n" + + INVALID + + "\n~~~\n" + ) + report = asyncio.run(utils.validate_mermaid_content(original, repair=True)) + assert report.diagrams == 2 and len(report.repairs) == 2 + assert report.content.count(FIXED) == 2 + assert report.content.count(INVALID) == 1 # example inside outer Markdown fence + assert len(parser) == 4 + assert len(utils.extract_mermaid_blocks(original)) == 2 + + +def test_unclosed_fence_and_other_languages_are_not_repaired(parser): + original = "```mermaidx\n" + INVALID + "\n```\n```mermaid\n" + INVALID + report = asyncio.run(utils.validate_mermaid_content(original, repair=True)) + assert report.diagrams == 1 and report.errors + assert report.content == original and not report.repairs + assert not mermaid_blocks("```python\n" + INVALID + "\n```") + + +def test_mixed_results_do_not_claim_all_valid(monkeypatch): + results = iter( + [ + DiagramResult("valid"), + DiagramResult("invalid", PARSE_ERROR.replace("'PS'", "'TEXT'")), + DiagramResult("unavailable", "offline"), + ] + ) + + async def parse(_): + return next(results) + + monkeypatch.setattr(utils, "_validate_diagram", parse) + report = asyncio.run(utils.validate_mermaid_content(markdown() * 3, repair=True)) + # First diagram stays untouched even though the source looks repairable. + assert report.content == markdown() * 3 + assert len(report.errors) == 1 and report.unavailable + message = report.format("mixed.md") + assert "syntax errors" in message and "unavailable" in message + assert "are syntax correct" not in message + + +@pytest.mark.parametrize("disabled", [True, False]) +def test_disabled_or_timed_out_validation_is_unavailable(monkeypatch, disabled): + async def no_local(_): + return None + + monkeypatch.setattr(utils, "_try_pythonmonkey_parse", no_local) + monkeypatch.setattr(utils, "_MERMAID_PY_BROKEN", disabled) + calls = [] + + async def timeout(*args, **kwargs): + calls.append(kwargs["timeout"]) + raise asyncio.TimeoutError + + def ready(*args): + fut = asyncio.get_running_loop().create_future() + fut.set_result(None) + return fut + + monkeypatch.setattr(utils, "_run_in_daemon_thread", ready) + monkeypatch.setattr(utils.asyncio, "wait_for", timeout) + report = asyncio.run(utils.validate_mermaid_content(markdown() * 2, repair=True)) + assert len(report.unavailable) == 2 and not report.errors and not report.repairs + assert report.content == markdown() * 2 + assert "syntax correct" not in report.format("x.md") + assert calls == ([] if disabled else [15.0]) + + +@pytest.mark.parametrize( + "code,message,expected", + [ + (400, PARSE_ERROR, "invalid"), + (503, PARSE_ERROR, "unavailable"), + (429, "Too many requests", "unavailable"), + (400, "Bad parameters", "unavailable"), + ], +) +def test_remote_adapter_classifies_http_responses(monkeypatch, code, message, expected): + import requests + + response = requests.Response() + response.status_code = code + response._content = message.encode() + monkeypatch.setattr(requests, "get", lambda *args, **kwargs: response) + assert utils._parse_via_mermaid_py(INVALID).status == expected + + +def test_remote_success_and_connection_error(monkeypatch): + import requests + + response = requests.Response() + response.status_code = 200 + response._content = b"" + monkeypatch.setattr(requests, "get", lambda *args, **kwargs: response) + assert utils._parse_via_mermaid_py(FIXED).status == "valid" + + def offline(*args, **kwargs): + raise requests.ConnectionError("offline") + + monkeypatch.setattr(requests, "get", offline) + assert utils._parse_via_mermaid_py(FIXED) == DiagramResult("unavailable", "offline") + + +def test_remote_diagnostics_keep_final_lexer_token(monkeypatch): + import requests + + response = requests.Response() + response.status_code = 400 + response._content = ("Parse error on line 2: " + "expected token, " * 50 + "got 'PS'").encode() + monkeypatch.setattr(requests, "get", lambda *args, **kwargs: response) + result = utils._parse_via_mermaid_py(INVALID) + assert result.status == "invalid" and "got 'PS'" in result.message + + +@pytest.mark.parametrize("local", ["", PARSE_ERROR, None]) +def test_local_parser_success_error_and_fallback(monkeypatch, local): + async def parse(_): + return local + + monkeypatch.setattr(utils, "_try_pythonmonkey_parse", parse) + monkeypatch.setattr(utils, "_MERMAID_PY_BROKEN", False) + calls = [] + + def remote(source): + calls.append(source) + return DiagramResult("valid") + + monkeypatch.setattr(utils, "_parse_via_mermaid_py", remote) + result = asyncio.run(utils._validate_diagram(INVALID)) + assert result.status == ("invalid" if local else "valid") + assert len(calls) == (1 if local is None else 0) + + +def test_missing_dependency_and_legacy_warning(monkeypatch): + async def no_local(_): + return None + + def missing(_): + raise ImportError("mermaid not installed") + + monkeypatch.setattr(utils, "_try_pythonmonkey_parse", no_local) + monkeypatch.setattr(utils, "_parse_via_mermaid_py", missing) + monkeypatch.setattr(utils, "_MERMAID_PY_BROKEN", False) + result = asyncio.run(utils.validate_single_diagram(FIXED, 1, 1)) + assert "validation unavailable" in result and "not installed" in result + + +def test_empty_markdown_does_not_use_parser(parser): + report = asyncio.run(utils.validate_mermaid_content("# No diagrams")) + assert report.format("x.md") == "No mermaid diagrams found in the file" + assert not parser + + +def test_valid_result_never_triggers_repair(monkeypatch): + calls = [] + + async def valid(source): + calls.append(source) + return DiagramResult("valid") + + monkeypatch.setattr(utils, "_validate_diagram", valid) + report = asyncio.run(utils.validate_mermaid_content(markdown(), repair=True)) + assert not report.errors and not report.repairs + assert report.content == markdown() and len(calls) == 1 diff --git a/tests/test_mermaid_writers.py b/tests/test_mermaid_writers.py new file mode 100644 index 00000000..3c8b455f --- /dev/null +++ b/tests/test_mermaid_writers.py @@ -0,0 +1,275 @@ +"""Mermaid integration across writing backends, prompts and overview generation.""" + +import asyncio +import json +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from codewiki.mcp.session import SessionStore +from codewiki.mcp.tools.doc_writer import handle_edit_doc_file, handle_write_doc_file +from codewiki.mcp.tools.prompt_server import _resolve_prompt +from codewiki.src.be import utils +from codewiki.src.be.agent_tools.str_replace_editor import EditTool, str_replace_editor +from codewiki.src.be.backend import UsageLedger +from codewiki.src.be.caw_toolkit import CawToolKit +from codewiki.src.be.documentation_generator import DocumentationGenerator +from codewiki.src.be.mermaid_support import DiagramResult, MERMAID_GUIDELINES +from codewiki.src.be.updater.prompts import ( + format_stale_fix_system_prompt, + format_update_system_prompt, +) + +INVALID = "```mermaid\nflowchart TD\nC{msg.error()?}\n```\n" +FIXED = INVALID.replace("C{msg.error()?}", 'C{"msg.error()?"}') +BROKEN = "```mermaid\nflowchart TD\nbroken\n```\n" + + +@pytest.fixture +def parser(monkeypatch): + calls = [] + + async def parse(source): + calls.append(source) + if "C{msg.error()?}" in source: + return DiagramResult("invalid", "Parse error on line 2: got 'PS'") + if "broken" in source: + return DiagramResult("invalid", "Parse error on line 2: got 'TEXT'") + return DiagramResult("valid") + + monkeypatch.setattr(utils, "_validate_diagram", parse) + return calls + + +@pytest.fixture(params=["pydantic", "caw"]) +def editor(request, tmp_path): + deps = SimpleNamespace( + registry={}, + absolute_docs_path=str(tmp_path), + absolute_repo_path=str(tmp_path / "repo"), + allowed_write_paths=None, + ) + toolkit = CawToolKit(deps, backend=None, allow_subagent=False) + + async def call(command, **kwargs): + if request.param == "caw": + return await toolkit.str_replace_editor("docs", command, path="x.md", **kwargs) + return await str_replace_editor( + SimpleNamespace(deps=deps), "docs", command, path="x.md", **kwargs + ) + + return call + + +def test_editor_repairs_and_returns_saved_snippet_and_undoes(tmp_path, editor, parser): + path = tmp_path / "x.md" + # Undo must restore even an invalid pre-existing document exactly. + original = "# Before\n" + INVALID + path.write_text(original) + + async def check(): + result = await editor("str_replace", old_str="# Before", new_str="# After") + assert path.read_text() == "# After\n" + FIXED + assert 'C{"msg.error()?"}' in result.split("Mermaid validation")[0] + assert "repaired and validated" in result + result = await editor("undo_edit") + assert "undone successfully" in result + assert path.read_text() == original + assert "syntax errors" in result and "repaired and validated" not in result + + asyncio.run(check()) + assert len(parser) == 3 + + +@pytest.mark.parametrize("command", ["create", "insert"]) +def test_editor_create_and_insert(tmp_path, editor, parser, command): + path = tmp_path / "x.md" + kwargs = {"file_text": INVALID} + if command == "insert": + path.write_text("# Title\n") + kwargs = {"insert_line": 1, "new_str": INVALID} + result = asyncio.run(editor(command, **kwargs)) + assert FIXED.strip() in path.read_text() + assert "repaired and validated" in result + + +def test_editor_failed_edits_and_views_do_not_repair(tmp_path, editor, parser): + path = tmp_path / "x.md" + path.write_text(INVALID) + + async def check(): + await editor("view") + await editor("str_replace", old_str="missing", new_str="replacement") + await editor("create", file_text="replace existing") + await editor("insert", insert_line=-1, new_str="bad position") + assert path.read_text() == INVALID + assert not parser + + asyncio.run(check()) + + +def test_failed_disk_write_does_not_create_history(tmp_path, monkeypatch): + path = tmp_path / "x.md" + path.write_text("old") + tool = EditTool({}, str(tmp_path)) + original_open = Path.open + + def fail(path, mode="r", *args, **kwargs): + if mode == "w": + raise OSError("disk unavailable") + return original_open(path, mode, *args, **kwargs) + + monkeypatch.setattr(Path, "open", fail) + tool(command="str_replace", path=str(path), old_str="old", new_str="new") + assert not tool.did_write and not tool._file_history + assert not any("has been edited" in line for line in tool.logs) + monkeypatch.undo() + assert path.read_text() == "old" + + +def test_mcp_write_edit_insert_undo_and_failed_edit(tmp_path, parser): + store = SessionStore() + session = store.create(str(tmp_path), str(tmp_path), {}, []) + args = {"session_id": session.session_id, "filename": "nested/x.md"} + path = tmp_path / "nested/x.md" + + async def check(): + created = json.loads(await handle_write_doc_file({**args, "content": INVALID}, store)) + assert path.read_text() == FIXED + assert "repaired and validated" in created["mermaid_validation"] + # Replacing the repaired label proves agents can use persisted content. + edited = json.loads( + await handle_edit_doc_file( + {**args, "command": "str_replace", "old_str": FIXED, "new_str": INVALID}, store + ) + ) + assert 'C{"msg.error()?"}' in edited["snippet"] and path.read_text() == FIXED + await handle_edit_doc_file({**args, "command": "undo"}, store) + assert path.read_text() == FIXED + await handle_edit_doc_file( + {**args, "command": "insert", "insert_line": 0, "new_str": "# Intro"}, store + ) + assert path.read_text().startswith("# Intro") + await handle_edit_doc_file({**args, "command": "undo"}, store) + assert path.read_text() == FIXED + before_calls = len(parser) + failed = json.loads( + await handle_edit_doc_file( + {**args, "command": "str_replace", "old_str": "missing"}, store + ) + ) + assert "error" in failed and len(parser) == before_calls + # Restoring a historical invalid document must not silently re-repair it. + session.registry["file_history"] = {str(path): [INVALID]} + result = json.loads(await handle_edit_doc_file({**args, "command": "undo"}, store)) + assert path.read_text() == INVALID and "syntax errors" in result["mermaid_validation"] + + asyncio.run(check()) + + +class Backend: + def __init__(self, replies): + self.replies = iter(replies) + self.calls = [] + self.usage_ledger = UsageLedger() + + def complete(self, prompt, system_prompt=None): + self.calls.append((prompt, system_prompt)) + self.usage_ledger.add("complete", "", {"input_tokens": 10}) + return next(self.replies) + + +def generator(tmp_path, replies): + gen = object.__new__(DocumentationGenerator) + gen.config = SimpleNamespace( + repo_path=str(tmp_path), layout="flat", get_prompt_addition=lambda: "Write in Portuguese." + ) + gen.backend = Backend(replies) + tree = {"parent": {"components": [], "children": {"leaf": {"components": [], "children": {}}}}} + (tmp_path / "module_tree.json").write_text(json.dumps(tree)) + return gen + + +@pytest.mark.parametrize("module_path,filename", [([], "overview.md"), (["parent"], "parent.md")]) +def test_overview_repairs_before_saving(tmp_path, parser, module_path, filename): + gen = generator(tmp_path, ["" + INVALID + ""]) + asyncio.run(gen.generate_parent_module_docs(module_path, str(tmp_path))) + assert (tmp_path / filename).read_text() == FIXED.strip() + assert len(gen.backend.calls) == 1 + + +def test_overview_retries_once_and_tracks_usage(tmp_path, parser): + gen = generator(tmp_path, [BROKEN, FIXED]) + asyncio.run(gen.generate_parent_module_docs([], str(tmp_path))) + assert (tmp_path / "overview.md").read_text() == FIXED.strip() + assert len(gen.backend.calls) == 2 + correction, system = gen.backend.calls[1] + assert "" in correction and "Parse error" in correction + assert MERMAID_GUIDELINES in correction and "Portuguese" in system + usage = gen.backend.usage_ledger.summary() + assert usage["n_calls"] == 2 and usage["by_phase"]["overview"]["calls"] == 2 + + +def test_invalid_overview_after_retry_is_not_saved(tmp_path, parser): + gen = generator(tmp_path, [BROKEN, BROKEN]) + with pytest.raises(RuntimeError, match="after one correction attempt"): + asyncio.run(gen.generate_parent_module_docs([], str(tmp_path))) + assert not (tmp_path / "overview.md").exists() + assert len(gen.backend.calls) == 2 + + +def test_overview_unavailable_warns_without_retry(tmp_path, monkeypatch, caplog): + async def unavailable(_): + return DiagramResult("unavailable", "HTTP 503") + + monkeypatch.setattr(utils, "_validate_diagram", unavailable) + gen = generator(tmp_path, [INVALID]) + asyncio.run(gen.generate_parent_module_docs([], str(tmp_path))) + assert (tmp_path / "overview.md").read_text() == INVALID.strip() + assert len(gen.backend.calls) == 1 + assert "validation unavailable" in caplog.text and "HTTP 503" in caplog.text + + +@pytest.mark.parametrize( + "prompt_type", ["system_leaf", "system_complex", "overview_module", "overview_repo"] +) +def test_generation_prompts_share_formatted_guidance(prompt_type): + prompt = _resolve_prompt( + prompt_type, + { + "module_name": "example", + "repo_name": "example", + "custom_instructions": "Write in Portuguese.", + }, + ) + assert MERMAID_GUIDELINES in prompt + assert "{{msg" not in prompt and 'C{"msg.error()?"}' in prompt + if prompt_type.startswith("system_"): + assert "Portuguese" in prompt + + +def test_updater_prompts_and_skill_share_guidance(): + assert MERMAID_GUIDELINES in format_update_system_prompt("leaf", "Portuguese") + assert MERMAID_GUIDELINES in format_stale_fix_system_prompt("Portuguese") + skill = (Path(__file__).parents[1] / "skills/codewiki-wiki-generator/SKILL.md").read_text() + for example in [ + 'C{"msg.error()?"}', + 'D["config[key]"]', + "A->>B: first#59; second", + 'A -- "start -- stop" --> B', + ]: + assert example in skill and example in MERMAID_GUIDELINES + + +def test_editor_undo_preserves_tabs_and_crlf(tmp_path, editor, parser): + path = tmp_path / "x.md" + original = ("# Before\n\ttext\n" + INVALID).replace("\n", "\r\n").encode() + path.write_bytes(original) + + async def check(): + await editor("str_replace", old_str="# Before", new_str="# After") + await editor("undo_edit") + + asyncio.run(check()) + assert path.read_bytes() == original From 5b234a941b42a2591eb5552cc4ca8fe5eb1efe8f Mon Sep 17 00:00:00 2001 From: anhnh2002 Date: Wed, 28 Oct 2026 09:30:31 +0700 Subject: [PATCH 2/2] fix: preserve inline edge text and isolate CLI toolkit tests --- codewiki/src/be/mermaid_support.py | 30 +++++++++++++++--- tests/mermaid/fixtures.json | 41 ++++++++++++++++++++++++ tests/test_mermaid_validation.py | 13 ++++++-- tests/test_mermaid_writers.py | 51 ++++++++++++++++++++++++++++-- 4 files changed, 125 insertions(+), 10 deletions(-) diff --git a/codewiki/src/be/mermaid_support.py b/codewiki/src/be/mermaid_support.py index 5e9a0cb9..30dc360d 100644 --- a/codewiki/src/be/mermaid_support.py +++ b/codewiki/src/be/mermaid_support.py @@ -115,8 +115,8 @@ def quote_flowchart_labels(source: str) -> tuple[str, list[tuple[str, str]]]: changes = [] output = [] for line in source.splitlines(keepends=True): - # Definitions, directives, comments, compound shapes, and inline edge - # labels need their own grammars. Never guess at their contents. + # Definitions, directives, comments and compound shapes need their + # own grammars. Never guess at their contents. if re.match( r"\s*(?:%%|---|subgraph\b|end\b|class\b|classDef\b|style\b|linkStyle\b|click\b)", line, @@ -138,9 +138,29 @@ def quote_flowchart_labels(source: str) -> tuple[str, list[tuple[str, str]]]: pieces.append(line[i : end + 1]) i = end + 1 continue - if re.match(r"[A-Za-z][A-Za-z0-9_]*(?:\(|>)", line[i:]) or re.match( - r"(?:--|==|-\.)\s+[^>|.\s]", line[i:] - ): + arrow = re.match(r"-->|---|--x|--o|==>|===|-\.->|-\.-", line[i:]) + if arrow: + pieces.append(arrow.group()) + i += len(arrow.group()) + continue + inline = re.match(r"--(?![->ox])|==(?![=>])|-\.(?![-.])", line[i:]) + if inline: + # Consume the edge text as one token, then resume at the + # target node. A code-like label is not a node definition. + closer = {"--": "-->", "==": "==>", "-.": ".->"}[inline.group()] + end = line.find(closer, i + len(inline.group())) + label = line[i + len(inline.group()) : end].strip() + quoted_label = ( + label.startswith('"') and label.endswith('"') and label.count('"') == 2 + ) + if end < 0 or (re.search(r"--|==|-\.", label) and not quoted_label): + pieces = [line] + del changes[first_change:] + break + pieces.append(line[i : end + len(closer)]) + i = end + len(closer) + continue + if re.match(r"[A-Za-z][A-Za-z0-9_]*(?:\(|>)", line[i:]): pieces = [line] del changes[first_change:] break diff --git a/tests/mermaid/fixtures.json b/tests/mermaid/fixtures.json index 04531b57..5855d4c6 100644 --- a/tests/mermaid/fixtures.json +++ b/tests/mermaid/fixtures.json @@ -154,5 +154,46 @@ "name": "thick inline edge text untouched", "source": "flowchart TD\nA == process(data) ==> B", "valid": true + }, + { + "name": "issue 141 exact reproduction", + "source": "flowchart TD\n A[\"consumer.poll(1.0)\"] --> B{msg is None?}\n B -- no --> C{msg.error()?}", + "valid": false, + "repaired": "flowchart TD\n A[\"consumer.poll(1.0)\"] --> B{msg is None?}\n B -- no --> C{\"msg.error()?\"}" + }, + { + "name": "inline edge code text preserved", + "source": "flowchart TD\nA -- state C{not a node} --> B{msg.error()?}", + "valid": false, + "repaired": "flowchart TD\nA -- state C{not a node} --> B{\"msg.error()?\"}" + }, + { + "name": "compact inline edge", + "source": "flowchart TD\nA--no-->C{msg.error()?}", + "valid": false, + "repaired": "flowchart TD\nA--no-->C{\"msg.error()?\"}" + }, + { + "name": "thick inline edge target", + "source": "flowchart TD\nA == state C{not a node} ==> B{msg.error()?}", + "valid": false, + "repaired": "flowchart TD\nA == state C{not a node} ==> B{\"msg.error()?\"}" + }, + { + "name": "dotted inline edge target", + "source": "flowchart TD\nA -. no .-> C{msg.error()?}", + "valid": false, + "repaired": "flowchart TD\nA -. no .-> C{\"msg.error()?\"}" + }, + { + "name": "quoted inline double dash target", + "source": "flowchart TD\nA -- \"start -- stop\" --> C{msg.error()?}", + "valid": false, + "repaired": "flowchart TD\nA -- \"start -- stop\" --> C{\"msg.error()?\"}" + }, + { + "name": "unclosed inline label untouched", + "source": "flowchart TD\nA -- C{msg.error()?}", + "valid": false } ] diff --git a/tests/test_mermaid_validation.py b/tests/test_mermaid_validation.py index da09198c..9ff983be 100644 --- a/tests/test_mermaid_validation.py +++ b/tests/test_mermaid_validation.py @@ -30,7 +30,7 @@ def parser(monkeypatch): async def parse(source): calls.append(source) - if INVALID in source.replace("\r\n", "\n"): + if "C{msg.error()?}" in source: return DiagramResult("invalid", PARSE_ERROR) if "broken" in source: return DiagramResult("invalid", "Parse error on line 2: got 'TEXT'") @@ -109,7 +109,7 @@ def test_concurrent_change_is_not_overwritten(tmp_path, monkeypatch): path.write_text(markdown()) async def parse(source): - if INVALID in source.replace("\r\n", "\n"): + if "C{msg.error()?}" in source: return DiagramResult("invalid", PARSE_ERROR) path.write_text("concurrent edit") return DiagramResult("valid") @@ -291,3 +291,12 @@ async def valid(source): report = asyncio.run(utils.validate_mermaid_content(markdown(), repair=True)) assert not report.errors and not report.repairs assert report.content == markdown() and len(calls) == 1 + + +def test_issue_exact_reproduction_repairs_only_diamond(parser): + case = next(case for case in FIXTURES if case["name"] == "issue 141 exact reproduction") + original = markdown(case["source"]) + report = asyncio.run(utils.validate_mermaid_content(original, repair=True)) + assert report.content == markdown(case["repaired"]) + assert not report.errors and len(report.repairs) == 1 + assert len(parser) == 2 diff --git a/tests/test_mermaid_writers.py b/tests/test_mermaid_writers.py index 3c8b455f..68de55a3 100644 --- a/tests/test_mermaid_writers.py +++ b/tests/test_mermaid_writers.py @@ -1,8 +1,10 @@ """Mermaid integration across writing backends, prompts and overview generation.""" import asyncio +import importlib.util import json from pathlib import Path +import sys from types import SimpleNamespace import pytest @@ -13,7 +15,6 @@ from codewiki.src.be import utils from codewiki.src.be.agent_tools.str_replace_editor import EditTool, str_replace_editor from codewiki.src.be.backend import UsageLedger -from codewiki.src.be.caw_toolkit import CawToolKit from codewiki.src.be.documentation_generator import DocumentationGenerator from codewiki.src.be.mermaid_support import DiagramResult, MERMAID_GUIDELINES from codewiki.src.be.updater.prompts import ( @@ -42,15 +43,43 @@ async def parse(source): return calls +def load_toolkit_class(monkeypatch): + if importlib.util.find_spec("caw") is not None: + from codewiki.src.be.caw_toolkit import CawToolKit + + return CawToolKit + + # CI installs the API-backend dependencies without the subscription CLI + # SDK. Stub only its registration boundary; execute the real editor body. + class ToolKit: + def __init_subclass__(cls, **kwargs): + pass + + def tool(**kwargs): + return lambda fn: fn + + monkeypatch.setitem(sys.modules, "caw", SimpleNamespace(ToolKit=ToolKit, tool=tool)) + path = Path(__file__).parents[1] / "codewiki/src/be/caw_toolkit.py" + spec = importlib.util.spec_from_file_location("_mermaid_test_caw_toolkit", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module.CawToolKit + + +@pytest.fixture +def toolkit_class(monkeypatch): + return load_toolkit_class(monkeypatch) + + @pytest.fixture(params=["pydantic", "caw"]) -def editor(request, tmp_path): +def editor(request, tmp_path, toolkit_class): deps = SimpleNamespace( registry={}, absolute_docs_path=str(tmp_path), absolute_repo_path=str(tmp_path / "repo"), allowed_write_paths=None, ) - toolkit = CawToolKit(deps, backend=None, allow_subagent=False) + toolkit = toolkit_class(deps, backend=None, allow_subagent=False) async def call(command, **kwargs): if request.param == "caw": @@ -273,3 +302,19 @@ async def check(): asyncio.run(check()) assert path.read_bytes() == original + + +def test_caw_editor_without_cli_sdk(tmp_path, monkeypatch, parser): + find_spec = importlib.util.find_spec + monkeypatch.setattr( + importlib.util, "find_spec", lambda name: None if name == "caw" else find_spec(name) + ) + toolkit = load_toolkit_class(monkeypatch) + deps = SimpleNamespace(registry={}, absolute_docs_path=str(tmp_path), allowed_write_paths=None) + result = asyncio.run( + toolkit(deps, None, False).str_replace_editor( + "docs", "create", path="x.md", file_text=INVALID + ) + ) + assert (tmp_path / "x.md").read_text() == FIXED + assert "repaired and validated" in result